/*
 * +================================================================================+
 *      __  ___  ______________  __  __  ______________   ____
 *     / / / / |/ /  _/_  __/\ \/ / / / / /_  __/  _/ /  / __/
 *    / /_/ /    // /  / /    \  / / /_/ / / / _/ // /___\ \
 *    \____/_/|_/___/ /_/     /_/  \____/ /_/ /___/____/___/
 *
 * +================================================================================+
 *
 *    "THE BEER-WARE LICENSE"
 *    -----------------------
 *
 *    As long as you retain this notice you can do whatever you want with this
 *    stuff. However it is expressly forbidden to sell or commercially distribute
 *    Unity Utils or parts of it apart from your games.
 *    If you meet any of the authors some day, and you think this stuff is
 *    worth it, you can buy them a beer in return.
 *
 *    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *    THE SOFTWARE.
 *
 * +================================================================================+
 *
 *    Authors:
 *    David Krummenacher (dave@timbookedtwo.com)
 *
 */

using System.Collections.Generic;
using UnityEngine;

namespace Utils.Behaviours {

	[AddComponentMenu("Utils/Behaviours/Pausable")]
	/// <summary>
	/// Pausable behaviour. Let's you pause and resume a game object.
	/// Pauses animators, rigidbodies, particle effects, audio sources and mono behaviours on the object.
	/// </summary>
	public class Pausable : MonoBehaviour {

		/// <summary>
		/// A list of all pausables.
		/// </summary>
		private static List<Pausable> Pausables = new List<Pausable>();

		/// <summary>
		/// Toggles the all pausables states.
		/// </summary>
		public static void TogglePauseAll() {
			foreach (Pausable pausable in Pausables) pausable.TogglePause();
		}

		/// <summary>
		/// Pauses all pausables.
		/// </summary>
		public static void PauseAll() {
			foreach (Pausable pausable in Pausables) pausable.Pause();
		}

		/// <summary>
		/// Resumes all pausables.
		/// </summary>
		public static void ResumeAll() {
			foreach (Pausable pausable in Pausables) pausable.Resume();
		}

		/// <summary>
		/// If the game object paused.
		/// </summary>
		private bool paused = false;
		/// <summary>
		/// Gets a value indicating whether this instance is paused.
		/// </summary>
		/// <value><c>true</c> if this instance is paused; otherwise, <c>false</c>.</value>
		public bool IsPaused {
			get { return paused; }
		}

		[Tooltip("Determines wheter to pause the animator component or not.")]
		/// <summary>
		/// Determines wheter to pause the animator component or not.
		/// </summary>
		public bool PauseAnimator = true;
		/// <summary>
		/// Temporary storage for the animator speed.
		/// </summary>
		private float animatorSpeed;
		/// <summary>
		/// Indicates whether the temporary values of the animator have been set.
		/// </summary>
		private bool animatorHasBeenSet = false;

		[Tooltip("Determines wheter to pause the rigidbody component or not.")]
		/// <summary>
		/// Determines wheter to pause the rigidbody component or not.
		/// </summary>
		public bool PauseRigidbody = true;
		/// <summary>
		/// Temporary storage for the rigidbody velocity.
		/// </summary>
		private Vector3 rigidbodyVelocity;
		/// <summary>
		/// Temporary storage for the rigidbody angular velocity.
		/// </summary>
		private Vector3 rigidbodyAngularVelocity;
		/// <summary>
		/// Temporary storage that dermines wheter the rigidbody was kinematic.
		/// </summary>
		private bool rigidbodyIsKinematic;
		/// <summary>
		/// Indicates whether the temporary values of the rigidbody have been set.
		/// </summary>
		private bool rigidbodyHasBeenSet = false;

		[Tooltip("Determines wheter to pause the particle system component or not.")]
		/// <summary>
		/// Determines wheter to pause the particle system component or not.
		/// </summary>
		public bool PauseParticleSystem = true;
		/// <summary>
		/// Temporary storage that dermines wheter the particle system was playing.
		/// </summary>
		private bool particleSystemIsPlaying;
		/// <summary>
		/// Indicates whether the temporary values of the paricle system have been set.
		/// </summary>
		private bool particleSystemHasBeenSet = false;

		[Tooltip("Determines wheter to pause the audio source component or not.")]
		/// <summary>
		/// Determines wheter to pause the audio source component or not.
		/// </summary>
		public bool PauseAudioSource = true;
		/// <summary>
		/// Temporary storage that dermines wheter the audio source was playing.
		/// </summary>
		private bool audioSourceIsPlaying;
		/// <summary>
		/// Indicates whether the temporary values of the audio source have been set.
		/// </summary>
		private bool audioSourceHasBeenSet = false;

		[Tooltip("Determines wheter to pause the renderer component or not.")]
		/// <summary>
		/// Determines wheter to pause the renderer component or not.
		/// </summary>
		public bool PauseRenderer = false;
		/// <summary>
		/// Temporary storage that dermines wheter the renderer was enabled.
		/// </summary>
		private bool rendererIsEnabled;
		/// <summary>
		/// Indicates whether the temporary values of the renderer have been set.
		/// </summary>
		private bool rendererHasBeenSet = false;

		[Tooltip("Determines wheter to pause the mono behaviour components or not.")]
		/// <summary>
		/// Determines wheter to pause the mono behaviour components or not.
		/// </summary>
		public bool PauseMonoBehaviours = true;

		[Tooltip("The mono behaviours that are excluded from pausing. Only mono behaviours from this object will be handled correctly.")]
		/// <summary>
		/// The mono behaviours that are excluded from pausing. Only mono behaviours from this object will be handled correctly.
		/// </summary>
		public List<MonoBehaviour> ExcludeMonoBehaviours;
		/// <summary>
		/// An array of all mono behaviours enabled state.
		/// </summary>
		private Dictionary<MonoBehaviour, bool> monoBehaviourEnabled = new Dictionary<MonoBehaviour, bool>();
		/// <summary>
		/// Indicates whether the temporary values for the mono behaviours have been set.
		/// </summary>
		private bool monoBehaviourHasBeenSet = false;

		void OnEnable() {
			// Add yourself to the pausables
			if (!Pausables.Contains(this)) Pausables.Add(this);
		}

		void OnDisable() {
			// Remove yourself from the pausables
			if (Pausables.Contains(this)) Pausables.Remove(this);
		}

		/// <summary>
		/// Toggles the pause state.
		/// </summary>
		public void TogglePause() {
			if (paused) Resume();
			else Pause();
		}

		/// <summary>
		/// Pauses this instance.
		/// </summary>
		public void Pause() {
			// Pause animator if possible
			if (PauseAnimator) {
				Animator animator = GetComponent<Animator>();
				if (animator != null) {
					animatorSpeed = animator.speed;
					animatorHasBeenSet = true;

					animator.speed = 0f;
				}
			}

			// Pause rigidbody if possible
			if (PauseRigidbody) {
				Rigidbody rigidbody = GetComponent<Rigidbody>();
				if (rigidbody != null) {
					rigidbodyVelocity = rigidbody.velocity;
					rigidbodyAngularVelocity = rigidbody.angularVelocity;
					rigidbodyIsKinematic = rigidbody.isKinematic;
					rigidbodyHasBeenSet = true;

					rigidbody.velocity = Vector3.zero;
					rigidbody.angularVelocity = Vector3.zero;
					rigidbody.isKinematic = true;
				}
			}

			// Pause particle system if possible
			if (PauseParticleSystem) {
				ParticleSystem particlesSystem = GetComponent<ParticleSystem>();
				if (particlesSystem != null) {
					particleSystemIsPlaying = particlesSystem.isPlaying;
					particleSystemHasBeenSet = true;

					if (particleSystemIsPlaying) particlesSystem.Pause(true);
				}
			}

			// Pause audio source if possible
			if (PauseAudioSource) {
				AudioSource audioSource = GetComponent<AudioSource>();
				if (audioSource != null) {
					audioSourceIsPlaying = audioSource.isPlaying;
					audioSourceHasBeenSet = true;

					if (audioSourceIsPlaying) audioSource.Pause();
				}
			}

			// Pause renderer if possible
			if (PauseRenderer) {
				Renderer renderer = GetComponent<Renderer>();
				if (renderer != null) {
					rendererIsEnabled = renderer.enabled;
					rendererHasBeenSet = true;
				}
			}

			// Pause mono behaviours if possible
			if (PauseMonoBehaviours) {
				monoBehaviourEnabled.Clear();
				foreach (MonoBehaviour behaviour in GetComponents<MonoBehaviour>()) {
					monoBehaviourEnabled.Add(behaviour, behaviour.enabled);

					if (behaviour == this || ExcludeMonoBehaviours.Contains(behaviour)) continue;
					behaviour.enabled = false;
				}
				monoBehaviourHasBeenSet = true;
			}

			// Indicate that this instance is paused
			paused = true;
		}

		/// <summary>
		/// Resumes this instance.
		/// </summary>
		public void Resume() {
			// Resume animator if possible
			if (animatorHasBeenSet) {
				Animator animator = GetComponent<Animator>();
				if (animator != null) {
					animator.speed = animatorSpeed;
					animatorHasBeenSet = false;
				}
			}

			// Resume rigidbody if possible
			if (rigidbodyHasBeenSet) {
				Rigidbody rigidbody = GetComponent<Rigidbody>();
				if (rigidbody != null) {
					rigidbody.velocity = rigidbodyVelocity;
					rigidbody.angularVelocity = rigidbodyAngularVelocity;
					rigidbody.isKinematic = rigidbodyIsKinematic;
					rigidbodyHasBeenSet = false;
				}
			}

			// Resume particle system if possible
			if (particleSystemHasBeenSet) {
				ParticleSystem particlesSystem = GetComponent<ParticleSystem>();
				if (particlesSystem != null) {
					if (particleSystemIsPlaying) particlesSystem.Play(true);
					particleSystemHasBeenSet = false;
				}
			}

			// Resume audio source if possible
			if (audioSourceHasBeenSet) {
				AudioSource audioSource = GetComponent<AudioSource>();
				if (audioSource != null) {
					if (audioSourceIsPlaying) audioSource.Play();
					audioSourceHasBeenSet = false;
				}
			}

			// Resume renderer if possible
			if (rendererHasBeenSet) {
				Renderer renderer = GetComponent<Renderer>();
				if (renderer != null) {
					renderer.enabled = rendererIsEnabled;
					rendererHasBeenSet = false;
				}
			}
			
			// Resume mono behaviours if possible
			if (monoBehaviourHasBeenSet) {
				foreach (MonoBehaviour behaviour in GetComponents<MonoBehaviour>()) {
					if (behaviour == this || ExcludeMonoBehaviours.Contains(behaviour)) continue;

					if (monoBehaviourEnabled.ContainsKey(behaviour)) {
						behaviour.enabled = monoBehaviourEnabled[behaviour];
					} else {
						Debug.LogWarning("Mono behaviours list for enabling states isn't available. Consider calling resume only after calling pause.");
					}
				}
				monoBehaviourEnabled.Clear();
				monoBehaviourHasBeenSet = false;
			}
			
			// Indicate that this instance isn't paused
			paused = false;
		}
	}
}
