using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace CartoonFX
{
	[RequireComponent(typeof(ParticleSystem))]
	public partial class CFXR_Effect : MonoBehaviour
	{
		[System.Serializable]
		public class CameraShake
		{
			public enum ShakeSpace
			{
				Screen,
				World
			}

			static public bool editorPreview = true;

			//--------------------------------------------------------------------------------------------------------------------------------

			public bool enabled = false;
			[Space]
			public bool useMainCamera = true;
			public List<Camera> cameras = new List<Camera>();
			[Space]
			public float delay = 0.0f;
			public float duration = 1.0f;
			public ShakeSpace shakeSpace = ShakeSpace.Screen;
			public Vector3 shakeStrength = new Vector3(0.1f, 0.1f, 0.1f);
			public AnimationCurve shakeCurve = AnimationCurve.Linear(0, 1, 1, 0);
			[Space]
			[Range(0, 0.1f)] public float shakesDelay = 0;

			[System.NonSerialized] public bool isShaking;
			Dictionary<Camera, Vector3> camerasPreRenderPosition = new Dictionary<Camera, Vector3>();
			Vector3 shakeVector;
			float delaysTimer;

			//--------------------------------------------------------------------------------------------------------------------------------
			// STATIC
			// Use static methods to dispatch the Camera callbacks, to ensure that ScreenShake components are called in an order in PreRender,
			// and in the _reverse_ order for PostRender, so that the final Camera position is the same as it is originally (allowing concurrent
			// screen shake to be active)

			static bool s_CallbackRegistered;
			static List<CameraShake> s_CameraShakes = new List<CameraShake>();

			static void OnPreRenderCamera_Static(Camera cam)
			{
				int count = s_CameraShakes.Count;
				for (int i = 0; i < count; i++)
				{
					var ss = s_CameraShakes[i];
					ss.onPreRenderCamera(cam);
				}
			}

			static void OnPostRenderCamera_Static(Camera cam)
			{
				int count = s_CameraShakes.Count;
				for (int i = count-1; i >= 0; i--)
				{
					var ss = s_CameraShakes[i];
					ss.onPostRenderCamera(cam);
				}
			}

			static void RegisterStaticCallback(CameraShake cameraShake)
			{
				s_CameraShakes.Add(cameraShake);

				if (!s_CallbackRegistered)
				{
					Camera.onPreRender += OnPreRenderCamera_Static;
					Camera.onPostRender += OnPostRenderCamera_Static;

					s_CallbackRegistered = true;
				}
			}

			static void UnregisterStaticCallback(CameraShake cameraShake)
			{
				s_CameraShakes.Remove(cameraShake);

				if (s_CallbackRegistered && s_CameraShakes.Count == 0)
				{
					Camera.onPreRender -= OnPreRenderCamera_Static;
					Camera.onPostRender -= OnPostRenderCamera_Static;

					s_CallbackRegistered = false;
				}
			}

			//--------------------------------------------------------------------------------------------------------------------------------

			void onPreRenderCamera(Camera cam)
			{
#if UNITY_EDITOR
				//add scene view camera if necessary
				if (SceneView.currentDrawingSceneView != null && SceneView.currentDrawingSceneView.camera == cam && !camerasPreRenderPosition.ContainsKey(cam))
				{
					camerasPreRenderPosition.Add(cam, cam.transform.localPosition);
				}
#endif

				if (isShaking && camerasPreRenderPosition.ContainsKey(cam))
				{
					camerasPreRenderPosition[cam] = cam.transform.localPosition;

					switch (shakeSpace)
					{
						case ShakeSpace.Screen: cam.transform.localPosition += cam.transform.rotation * shakeVector; break;
						case ShakeSpace.World: cam.transform.localPosition += shakeVector; break;
					}
				}
			}

			void onPostRenderCamera(Camera cam)
			{
				if (camerasPreRenderPosition.ContainsKey(cam))
				{
					cam.transform.localPosition = camerasPreRenderPosition[cam];
				}
			}

			public void fetchCameras()
			{
#if UNITY_EDITOR
				if (!EditorApplication.isPlayingOrWillChangePlaymode)
				{
					return;
				}
#endif

				foreach (var cam in cameras)
				{
					if (cam == null) continue;

					camerasPreRenderPosition.Remove(cam);
				}

				cameras.Clear();

				if (useMainCamera && Camera.main != null)
				{
					cameras.Add(Camera.main);
				}

				foreach (var cam in cameras)
				{
					if (cam == null) continue;

					if (!camerasPreRenderPosition.ContainsKey(cam))
					{
						camerasPreRenderPosition.Add(cam, Vector3.zero);
					}
				}
			}

			public void StartShake()
			{
				if (isShaking)
				{
					StopShake();
				}

				isShaking = true;
				RegisterStaticCallback(this);
			}

			public void StopShake()
			{
				isShaking = false;
				shakeVector = Vector3.zero;
				UnregisterStaticCallback(this);
			}

			public void animate(float time)
			{
#if UNITY_EDITOR
				if (!editorPreview && !EditorApplication.isPlaying)
				{
					shakeVector = Vector3.zero;
					return;
				}
#endif

				float totalDuration = duration + delay;
				if (time < totalDuration)
				{
					if (time < delay)
					{
						return;
					}

					if (!isShaking)
					{
						this.StartShake();
					}

					// duration of the camera shake
					float delta = Mathf.Clamp01(time/totalDuration);

					// delay between each camera move
					if (shakesDelay > 0)
					{
						delaysTimer += Time.deltaTime;
						if (delaysTimer < shakesDelay)
						{
							return;
						}
						else
						{
							while (delaysTimer >= shakesDelay)
							{
								delaysTimer -= shakesDelay;
							}
						}
					}

					var randomVec = new Vector3(Random.value, Random.value, Random.value);
					var shakeVec = Vector3.Scale(randomVec, shakeStrength) * (Random.value > 0.5f ? -1 : 1);
					shakeVector = Vector3.Lerp(Vector3.zero, shakeVec, shakeCurve.Evaluate(delta));
					//shakeVector = shakeVec * shakeCurve.Evaluate(delta); // TODO this is the same?
				}
				else if (isShaking)
				{
					StopShake();
				}
			}
		}
	}
}