using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace MoreMountains.Tools
{
	/// <summary>
	/// This class stores all the info related to items in a playlist
	/// </summary>

	public struct MMPlaylistPlayEvent
	{
		static private event Delegate OnEvent;
		[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
		static public void Register(Delegate callback) { OnEvent += callback; }
		static public void Unregister(Delegate callback) { OnEvent -= callback; }

		public delegate void Delegate(int channel);
		static public void Trigger(int channel)
		{
			OnEvent?.Invoke(channel);
		}
	}
	public struct MMPlaylistStopEvent
	{
		static private event Delegate OnEvent;
		[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
		static public void Register(Delegate callback) { OnEvent += callback; }
		static public void Unregister(Delegate callback) { OnEvent -= callback; }

		public delegate void Delegate(int channel);
		static public void Trigger(int channel)
		{
			OnEvent?.Invoke(channel);
		}
	}
	public struct MMPlaylistPauseEvent
	{
		static private event Delegate OnEvent;
		[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
		static public void Register(Delegate callback) { OnEvent += callback; }
		static public void Unregister(Delegate callback) { OnEvent -= callback; }

		public delegate void Delegate(int channel);
		static public void Trigger(int channel)
		{
			OnEvent?.Invoke(channel);
		}
	}
	public struct MMPlaylistPlayNextEvent
	{
		static private event Delegate OnEvent;
		[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
		static public void Register(Delegate callback) { OnEvent += callback; }
		static public void Unregister(Delegate callback) { OnEvent -= callback; }

		public delegate void Delegate(int channel);
		static public void Trigger(int channel)
		{
			OnEvent?.Invoke(channel);
		}
	}
	public struct MMPlaylistPlayPreviousEvent
	{
		static private event Delegate OnEvent;
		[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
		static public void Register(Delegate callback) { OnEvent += callback; }
		static public void Unregister(Delegate callback) { OnEvent -= callback; }

		public delegate void Delegate(int channel);
		static public void Trigger(int channel)
		{
			OnEvent?.Invoke(channel);
		}
	}

	public struct MMPlaylistPlayIndexEvent
	{
		static private event Delegate OnEvent;
		[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
		static public void Register(Delegate callback) { OnEvent += callback; }
		static public void Unregister(Delegate callback) { OnEvent -= callback; }

		public delegate void Delegate(int channel, int index);
		static public void Trigger(int channel, int index)
		{
			OnEvent?.Invoke(channel, index);
		}
	}

	[System.Serializable]
	public class MMPlaylistSong
	{
		/// the audiosource that contains the audio clip we want to play
		public AudioSource TargetAudioSource;
		/// the min (when it's off) and max (when it's playing) volume for this source
		[MMVector("Min", "Max")]
		public Vector2 Volume = new Vector2(1f, 1f);
		/// a random delay in seconds to apply, between its RMin and RMax
		[MMVector("RMin", "RMax")]
		public Vector2 InitialDelay = Vector2.zero;
		/// a random crossfade duration (in seconds) to apply when transitioning to this song, between its RMin and RMax
		[MMVector("RMin", "RMax")]
		public Vector2 CrossFadeDuration = new Vector2(2f, 2f);
		/// a random pitch to apply to this song, between its RMin and RMax
		[MMVector("RMin", "RMax")]
		public Vector2 Pitch = Vector2.one;
		/// the stereo pan for this song
		[Range(-1f, 1f)]
		public float StereoPan = 0f;
		/// the spatial blend for this song (0 is 2D, 1 is 3D)
		[Range(0f, 1f)]
		public float SpatialBlend = 0f;
		/// whether this song should loop or not
		public bool Loop = false;
		/// whether this song is playing right now or not
		[MMReadOnly]
		public bool Playing = false;
		/// whether this song is fading right now or not
		[MMReadOnly]
		public bool Fading = false;

		[MMHidden]
		public bool _initialized;

		public virtual void Initialization()
		{
			if (_initialized)
			{
				return;
			}
			
			this.Volume = new Vector2(1f, 1f);
			this.InitialDelay = Vector2.zero;
			this.CrossFadeDuration = new Vector2(2f, 2f);
			this.Pitch = Vector2.one;
			this.StereoPan = 0f;
			this.SpatialBlend = 0f;
			this.Loop = false;
			this._initialized = true;
		}
	}

	/// <summary>
	/// Use this class to play audiosources (usually background music but feel free to use that for anything) in sequence, with optional crossfade between songs
	/// </summary>
	[AddComponentMenu("More Mountains/Tools/Audio/MMPlaylist")]
	[MMRequiresConstantRepaint]
	public class MMPlaylist : MMMonoBehaviour
	{
		/// the possible states this playlist can be in
		public enum PlaylistStates
		{
			Idle,
			Playing,
			Paused
		}
		
		[MMInspectorGroup("Playlist Songs", true, 18)]
        
		/// the channel on which to broadcast orders for this playlist
		[Tooltip("the channel on which to broadcast orders for this playlist")]
		public int Channel = 0;
		/// the songs that this playlist will play
		[Tooltip("the songs that this playlist will play")]
		public List<MMPlaylistSong> Songs;

		[MMInspectorGroup("Settings", true, 13)]
		
		/// whether this should play in random order or not
		[Tooltip("whether this should play in random order or not")]
		public bool RandomOrder = false;
		/// if this is true, random seed will be randomized by the system clock
		[Tooltip("if this is true, random seed will be randomized by the system clock")]
		[MMCondition("RandomOrder", true)]
		public bool RandomizeOrderSeed = true;
		/// whether this playlist should play and loop as a whole forever or not
		[Tooltip("whether this playlist should play and loop as a whole forever or not")]
		public bool Endless = true;
		/// whether this playlist should auto play on start or not
		[Tooltip("whether this playlist should auto play on start or not")]
		public bool PlayOnStart = true;
		/// a global volume multiplier to apply when playing a song
		[Tooltip("a global volume multiplier to apply when playing a song")]
		public float VolumeMultiplier = 1f;
		/// if this is true, this playlist will automatically pause/resume OnApplicationPause, useful if you've prevented your game from running in the background
		[Tooltip("if this is true, this playlist will automatically pause/resume OnApplicationPause, useful if you've prevented your game from running in the background")]
		public bool AutoHandleApplicationPause = true;
		
		[MMInspectorGroup("Persistence", true, 32)]
		/// if this is true, this playlist will persist from scene to scene
		[Tooltip("if this is true, this playlist will persist from scene to scene")]
		public bool Persistent = false;
		/// if this is true, this singleton will auto detach if it finds itself parented on awake
		[Tooltip("if this is true, this singleton will auto detach if it finds itself parented on awake")]
		[MMCondition("Persistent", true)]
		public bool AutomaticallyUnparentOnAwake = true;

		[MMInspectorGroup("Status", true, 14)]
		
		/// the current state of the playlist, debug display only
		[Tooltip("the current state of the playlist, debug display only")]
		[MMReadOnly]
		public PlaylistStates DebugCurrentState = PlaylistStates.Idle;
		/// the index we're currently playing
		[Tooltip("the index we're currently playing")]
		[MMReadOnly]
		public int CurrentlyPlayingIndex = -1;
		/// the name of the song that is currently playing
		[Tooltip("the name of the song that is currently playing")]
		[MMReadOnly]
		public string CurrentSongName;
		/// the current state of this playlist
		[MMReadOnly]
		public MMStateMachine<MMPlaylist.PlaylistStates> PlaylistState;

		[MMInspectorGroup("Tests", true, 15)]
		
		/// a play test button
		[MMInspectorButton("Play")]
		public bool PlayButton;
		/// a pause test button
		[MMInspectorButton("Pause")]
		public bool PauseButton;
		/// a stop test button
		[MMInspectorButton("Stop")]
		public bool StopButton;
		/// a next song test button
		[MMInspectorButton("PlayNextSong")]
		public bool NextButton;
		/// the index of the song to play when pressing the PlayTargetSong button
		[Tooltip("the index of the song to play when pressing the PlayTargetSong button")]
		public int TargetSongIndex = 0;
		/// a next song test button
		[MMInspectorButton("PlayTargetSong")]
		public bool TargetSongButton;
        
		protected int _songsPlayedSoFar = 0;
		protected int _songsPlayedThisCycle = 0;
		protected Coroutine _coroutine;
		protected bool _shouldResumeOnApplicationPause = false;
		
		public static bool HasInstance => _instance != null;
		public static MMPlaylist Current => _instance;
		protected static MMPlaylist _instance;
		protected bool _enabled;
		
		/// <summary>
		/// Singleton design pattern
		/// </summary>
		/// <value>The instance.</value>
		public static MMPlaylist Instance
		{
			get
			{
				if (_instance == null)
				{
					_instance = FindObjectOfType<MMPlaylist> ();
					if (_instance == null)
					{
						GameObject obj = new GameObject ();
						obj.name = typeof(MMPlaylist).Name + "_AutoCreated";
						_instance = obj.AddComponent<MMPlaylist> ();
					}
				}
				return _instance;
			}
		}
		
		/// <summary>
		/// On awake, we check if there's already a copy of the object in the scene. If there's one, we destroy it.
		/// </summary>
		protected virtual void Awake ()
		{
			InitializeSingleton();
		}

		/// <summary>
		/// Initializes the singleton.
		/// </summary>
		protected virtual void InitializeSingleton()
		{
			if (!Application.isPlaying)
			{
				return;
			}

			if (!Persistent)
			{
				return;
			}

			if (AutomaticallyUnparentOnAwake)
			{
				this.transform.SetParent(null);
			}

			if (_instance == null)
			{
				//If I am the first instance, make me the Singleton
				_instance = this;
				DontDestroyOnLoad (transform.gameObject);
				_enabled = true;
			}
			else
			{
				//If a Singleton already exists and you find
				//another reference in scene, destroy it!
				if(this != _instance)
				{
					Destroy(this.gameObject);
				}
			}
		}
        
		/// <summary>
		/// On Start we initialize our playlist
		/// </summary>
		protected virtual void Start()
		{
			Initialization();
		}

		/// <summary>
		/// On init we initialize our state machine and start playing if needed
		/// </summary>
		protected virtual void Initialization()
		{
			if (RandomOrder && RandomizeOrderSeed)
			{
				Random.InitState(System.Environment.TickCount);
			}
			_songsPlayedSoFar = 0;
			PlaylistState = new MMStateMachine<MMPlaylist.PlaylistStates>(this.gameObject, true);
			ChangePlaylistState(PlaylistStates.Idle);
			if (Songs.Count == 0)
			{
				return;
			}
			if (PlayOnStart)
			{
				PlayFirstSong();
			}
		}

		protected virtual void ChangePlaylistState(PlaylistStates newState)
		{
			PlaylistState.ChangeState(newState);
			DebugCurrentState = newState;
		}

		/// <summary>
		/// Picks and plays the first song
		/// </summary>
		protected virtual void PlayFirstSong()
		{
			_songsPlayedThisCycle = 0;
			CurrentlyPlayingIndex = -1;
			int newIndex = PickNextIndex();
			_coroutine = StartCoroutine(PlaySong(newIndex));
		}

		/// <summary>
		/// Plays a new song in the playlist, and stops / fades the previous one
		/// </summary>
		/// <param name="index"></param>
		/// <returns></returns>
		protected virtual IEnumerator PlaySong(int index)
		{
			// if we don't have a song, we stop
			if (Songs.Count == 0)
			{
				yield break;
			}

			// if we've played all our songs, we stop
			if (!Endless && (_songsPlayedThisCycle > Songs.Count))
			{
				yield break;
			}

			if (_coroutine != null)
			{
				StopCoroutine(_coroutine);
			}
            
			// we stop our current song                        
			if ((PlaylistState.CurrentState == PlaylistStates.Playing) 
			    && (index >= 0 && index < Songs.Count)
			    && !Songs[index].TargetAudioSource.isPlaying)
			{
				StartCoroutine(Fade(CurrentlyPlayingIndex,
					Random.Range(Songs[index].CrossFadeDuration.x, Songs[index].CrossFadeDuration.y),
					Songs[CurrentlyPlayingIndex].Volume.y * VolumeMultiplier,
					Songs[CurrentlyPlayingIndex].Volume.x * VolumeMultiplier,
					true));
			}

			// we stop all other coroutines
			if ((CurrentlyPlayingIndex >= 0) && (Songs.Count > CurrentlyPlayingIndex))
			{
				foreach (MMPlaylistSong song in Songs)
				{
					if (song != Songs[CurrentlyPlayingIndex])
					{
						song.Fading = false;
					}
				}
			}     
            
			if (index < 0 || index >= Songs.Count)
			{
				yield break;
			}

			// initial delay
			yield return MMCoroutine.WaitFor(Random.Range(Songs[index].InitialDelay.x, Songs[index].InitialDelay.y));

			if (Songs[index].TargetAudioSource == null)
			{
				Debug.LogError(this.name + " : the playlist song you're trying to play is null");
				yield break;
			}

			Songs[index].TargetAudioSource.pitch = Random.Range(Songs[index].Pitch.x, Songs[index].Pitch.y);
			Songs[index].TargetAudioSource.panStereo = Songs[index].StereoPan;
			Songs[index].TargetAudioSource.spatialBlend = Songs[index].SpatialBlend;
			Songs[index].TargetAudioSource.loop = Songs[index].Loop;
            
			// fades the new song's volume
			StartCoroutine(Fade(index,
				Random.Range(Songs[index].CrossFadeDuration.x, Songs[index].CrossFadeDuration.y),
				Songs[index].Volume.x * VolumeMultiplier,
				Songs[index].Volume.y * VolumeMultiplier,
				false));	

			// starts the new song
			Songs[index].TargetAudioSource.Play();

			// updates our state
			CurrentSongName = Songs[index].TargetAudioSource.clip.name;
			ChangePlaylistState(PlaylistStates.Playing);
			Songs[index].Playing = true;
			CurrentlyPlayingIndex = index;
			_songsPlayedSoFar++;
			_songsPlayedThisCycle++;

			while (Songs[index].TargetAudioSource.isPlaying || (PlaylistState.CurrentState == PlaylistStates.Paused) || _shouldResumeOnApplicationPause)
			{
				yield return null;
			}

			if (PlaylistState.CurrentState != PlaylistStates.Playing)
			{
				yield break;
			}
            
			if (_songsPlayedSoFar < Songs.Count)
			{
				_coroutine = StartCoroutine(PlaySong(PickNextIndex()));
			}
			else
			{
				if (Endless)
				{
					_coroutine = StartCoroutine(PlaySong(PickNextIndex()));
				}
				else
				{
					ChangePlaylistState(PlaylistStates.Idle);
				}
			}
		}

		/// <summary>
		/// Fades an audiosource in or out, optionnally stopping it at the end
		/// </summary>
		/// <param name="source"></param>
		/// <param name="duration"></param>
		/// <param name="initialVolume"></param>
		/// <param name="endVolume"></param>
		/// <param name="stopAtTheEnd"></param>
		/// <returns></returns>
		protected virtual IEnumerator Fade(int index, float duration, float initialVolume, float endVolume, bool stopAtTheEnd)
		{
			if (index < 0 || index >= Songs.Count)
			{
				yield break;
			}

			float startTimestamp = Time.time;
			float progress = 0f;
			Songs[index].Fading = true;

			while ((Time.time - startTimestamp < duration) && (Songs[index].Fading))
			{
				progress = MMMaths.Remap(Time.time - startTimestamp, 0f, duration, 0f, 1f);
				Songs[index].TargetAudioSource.volume = Mathf.Lerp(initialVolume, endVolume, progress);
				yield return null;
			}

			Songs[index].TargetAudioSource.volume = endVolume;

			if (stopAtTheEnd)
			{
				Songs[index].TargetAudioSource.Stop();
				Songs[index].Playing = false;
				Songs[index].Fading = false;
			}
		}

		/// <summary>
		/// Picks the next song to play
		/// </summary>
		/// <returns></returns>
		protected virtual int PickNextIndex()
		{
			if (Songs.Count == 0)
			{
				return -1;
			}

			int newIndex = CurrentlyPlayingIndex;
			if (RandomOrder)
			{
				while (newIndex == CurrentlyPlayingIndex)
				{
					newIndex = Random.Range(0, Songs.Count);
				}                
			}
			else
			{
				newIndex = (CurrentlyPlayingIndex + 1) % Songs.Count;
			}

			return newIndex;
		}
		/// <summary>
		/// Picks the previous song to play
		/// </summary>
		/// <returns></returns>
		protected virtual int PickPreviousIndex()
		{
			if (Songs.Count == 0)
			{
				return -1;
			}

			int newIndex = CurrentlyPlayingIndex;
			if (RandomOrder)
			{
				while (newIndex == CurrentlyPlayingIndex)
				{
					newIndex = Random.Range(0, Songs.Count);
				}                
			}
			else
			{
				newIndex = (CurrentlyPlayingIndex - 1);
				if (newIndex < 0)
				{
					newIndex = Songs.Count - 1;
				}
			}

			return newIndex;
		}

		/// <summary>
		/// Plays either the first song or resumes playing a paused one
		/// </summary>
		public virtual void Play()
		{
			switch (PlaylistState.CurrentState)
			{
				case PlaylistStates.Idle:
					PlayFirstSong();
					break;

				case PlaylistStates.Paused:
					Songs[CurrentlyPlayingIndex].TargetAudioSource.UnPause();
					ChangePlaylistState(PlaylistStates.Playing);
					break;

				case PlaylistStates.Playing:
					// do nothing
					break;
			}
		}

		public virtual void PlayAtIndex(int songIndex)
		{
			_coroutine = StartCoroutine(PlaySong(songIndex));
		}
        
		/// <summary>
		/// Pauses the current song
		/// </summary>
		public virtual void Pause()
		{
			if (PlaylistState.CurrentState != PlaylistStates.Playing)
			{
				return;
			}

			Songs[CurrentlyPlayingIndex].TargetAudioSource.Pause();
			ChangePlaylistState(PlaylistStates.Paused);
		}

		/// <summary>
		/// Stops the playlist
		/// </summary>
		public virtual void Stop()
		{
			if (PlaylistState.CurrentState != PlaylistStates.Playing)
			{
				return;
			} 
	        
			Songs[CurrentlyPlayingIndex].TargetAudioSource.Stop();
			Songs[CurrentlyPlayingIndex].Playing = false;
			Songs[CurrentlyPlayingIndex].Fading = false;
			CurrentlyPlayingIndex = -1;
			ChangePlaylistState(PlaylistStates.Idle);
		}

		/// <summary>
		/// Plays the next song in the playlist
		/// </summary>
		public virtual void PlayNextSong()
		{
			int newIndex = PickNextIndex();
			_coroutine = StartCoroutine(PlaySong(newIndex));
		}

		/// <summary>
		/// Plays the previous song in the playlist
		/// </summary>
		public virtual void PlayPreviousSong()
		{
			int newIndex = PickPreviousIndex();
			_coroutine = StartCoroutine(PlaySong(newIndex));
		}

		protected virtual void PlayTargetSong()
		{
			int newIndex = Mathf.Clamp(TargetSongIndex, 0, Songs.Count - 1);
			PlayAtIndex(newIndex);
		}

		protected virtual void OnPlayEvent(int channel)
		{
			if (channel != Channel) { return; }
			Play();
		}

		protected virtual void OnPauseEvent(int channel)
		{
			if (channel != Channel) { return; }
			Pause();
		}

		protected virtual void OnStopEvent(int channel)
		{
			if (channel != Channel) { return; }
			Stop();
		}

		protected virtual void OnPlayNextEvent(int channel)
		{
			if (channel != Channel) { return; }
			PlayNextSong();
		}

		protected virtual void OnPlayPreviousEvent(int channel)
		{
			if (channel != Channel) { return; }
			PlayPreviousSong();
		}

		protected virtual void OnPlayIndexEvent(int channel, int index)
		{
			if (channel != Channel) { return; }
			_coroutine = StartCoroutine(PlaySong(index));
		}

		/// <summary>
		/// On enable, starts listening for playlist events
		/// </summary>
		protected virtual void OnEnable()
		{
			MMPlaylistPauseEvent.Register(OnPauseEvent);
			MMPlaylistPlayEvent.Register(OnPlayEvent);
			MMPlaylistPlayNextEvent.Register(OnPlayNextEvent);
			MMPlaylistPlayPreviousEvent.Register(OnPlayPreviousEvent);
			MMPlaylistStopEvent.Register(OnStopEvent);
			MMPlaylistPlayIndexEvent.Register(OnPlayIndexEvent);
		}

		/// <summary>
		/// On disable, stops listening for playlist events
		/// </summary>
		protected virtual void OnDisable()
		{
			MMPlaylistPauseEvent.Unregister(OnPauseEvent);
			MMPlaylistPlayEvent.Unregister(OnPlayEvent);
			MMPlaylistPlayNextEvent.Unregister(OnPlayNextEvent);
			MMPlaylistPlayPreviousEvent.Unregister(OnPlayPreviousEvent);
			MMPlaylistStopEvent.Unregister(OnStopEvent);
			MMPlaylistPlayIndexEvent.Unregister(OnPlayIndexEvent);
		}
        
		protected bool _firstDeserialization = true;
		protected int _listCount = 0;

		/// <summary>
		/// On Validate, we check if our array has changed and if yes we initialize our new elements
		/// </summary>
		protected virtual void OnValidate()
		{
			if (_firstDeserialization)
			{
				if (Songs == null)
				{
					_listCount = 0;
					_firstDeserialization = false;
				}
				else
				{
					_listCount = Songs.Count;
					_firstDeserialization = false;
				}                
			}
			else
			{
				if (Songs.Count != _listCount)
				{
					if (Songs.Count > _listCount)
					{
						foreach(MMPlaylistSong song in Songs)
						{
							song.Initialization();
						}                            
					}
					_listCount = Songs.Count;
				}
			}
		}

		/// <summary>
		/// On ApplicationPause, we pause the playlist and resume it afterwards
		/// </summary>
		/// <param name="pauseStatus"></param>
		protected virtual void OnApplicationPause(bool pauseStatus)
		{
			if (!AutoHandleApplicationPause)
			{
				return;
			}
			
			if (pauseStatus && PlaylistState.CurrentState == PlaylistStates.Playing)
			{
				Pause();
				_shouldResumeOnApplicationPause = true;
			}

			if (!pauseStatus && _shouldResumeOnApplicationPause)
			{
				_shouldResumeOnApplicationPause = false;
				Play();
			}
		}
	}
}