using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MoreMountains.Tools;
using System;
using UnityEngine.Events;
using UnityEngine.SceneManagement;

namespace MoreMountains.Tools
{
	/// <summary>
	/// A static class used to save / load peaks once they've been computed
	/// </summary>
	public static class PeaksSaver
	{
		public static float[] Peaks;
	}

	/// <summary>
	/// An event you can listen to that will get automatically triggered for every remapped beat
	/// </summary>
	public struct MMBeatEvent
	{
		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(string name, float value);
		static public void Trigger(string name, float value)
		{
			OnEvent?.Invoke(name, value);
		}
	}

	[Serializable]
	public class Beat
	{
		public string Name = "Beat";
		public enum Modes { Raw, Normalized, BufferedRaw, BufferedNormalized, Amplitude, NormalizedAmplitude, AmplitudeBuffered, NormalizedAmplitudeBuffered }
		// remapped will send beat events when a threshold is passed, live just updates the value with whatever value is reading right now
		public enum BeatValueModes { Remapped, Live }

		public Modes Mode = Modes.BufferedNormalized;
		public BeatValueModes BeatValueMode = BeatValueModes.Remapped;

		[MMEnumCondition("Mode", (int)Modes.Raw, (int)Modes.Normalized, (int)Modes.BufferedRaw, (int)Modes.BufferedNormalized)]
		public Color BeatColor = Color.cyan;
		public int BandID = 0;
		public float Threshold = 0.5f;
		public float MinimumTimeBetweenBeats = 0.25f;

		[MMEnumCondition("BeatValueMode", (int)BeatValueModes.Remapped)]
		public float RemappedAttack = 0.05f;
		[MMEnumCondition("BeatValueMode", (int)BeatValueModes.Remapped)]
		public float RemappedDecay = 0.2f;

		[MMReadOnly]
		public bool BeatThisFrame;
		[MMReadOnly]
		public float CurrentValue;
		[HideInInspector]
		public float _previousValue;
		[HideInInspector]
		public float _lastBeatAt;
		[HideInInspector]
		public float _lastBeatValue;
		[HideInInspector]
		public bool _initialized = false;

		public UnityEvent OnBeat;

		public void InitializeIfNeeded(int id, int bandID)
		{
			if (!_initialized)
			{
				Mode = Modes.Normalized;
				BeatValueMode = BeatValueModes.Remapped;
				Name = "Beat " + id;
				BeatColor = MMColors.RandomColor();
				BandID = bandID;
				Threshold = 0.3f + id * 0.02f;
				if (Threshold > 0.6f) { Threshold -= 0.5f; }
				Threshold = Threshold % 1f;
				MinimumTimeBetweenBeats = 0.25f + id * 0.02f;
				RemappedAttack = 0.05f + id * 0.01f;
				RemappedDecay = 0.2f + id * 0.01f;
				_initialized = true;
			}
		}
	}

	/// <summary>
	/// This component lets you pick an audio source (either global : the whole scene's audio, a unique source, or the 
	/// microphone), and will cut it into chunks that you can then use to emit beat events, that other objects can consume and act upon. 
	/// The sample interval is the frequency at which sound will be analyzed, the amount of spectrum samples will determine the 
	/// accuracy of the sampling, the window defines the method used to reduce leakage, and the number of bands 
	/// will determine in how many bands you want to cut the sound. The more bands, the more levers you'll have to play with afterwards.
	/// In general, for all of these settings, higher values mean better quality and lower performance. The buffer speed determines how 
	/// fast buffered band levels readjust.
	/// </summary>
	[AddComponentMenu("More Mountains/Tools/Audio/MMAudioAnalyzer")]
	public class MMAudioAnalyzer : MonoBehaviour
	{
		public enum Modes { Global, AudioSource, Microphone }

		[Header("Source")]
		[MMInformation("This component lets you pick an audio source (either global : the whole scene's audio, a unique source, or the " +
		               "microphone), and will cut it into chunks that you can then use to emit beat events, that other objects can consume and act upon. " +
		               "The sample interval is the frequency at which sound will be analyzed, the amount of spectrum samples will determine the " +
		               "accuracy of the sampling, the window defines the method used to reduce leakage, and the number of bands " +
		               "will determine in how many bands you want to cut the sound. The more bands, the more levers you'll have to play with afterwards." +
		               "In general, for all of these settings, higher values mean better quality and lower performance. The buffer speed determines how " +
		               "fast buffered band levels readjust.", MoreMountains.Tools.MMInformationAttribute.InformationType.Info, false)]
		[MMReadOnlyWhenPlaying]
		public Modes Mode = Modes.Global;
		[MMEnumCondition("Mode", (int)Modes.AudioSource)]
		[MMReadOnlyWhenPlaying]
		public AudioSource TargetAudioSource;
		[MMEnumCondition("Mode", (int)Modes.Microphone)]
		public int MicrophoneID = 0;

		[Header("Sampling")]
		[MMReadOnlyWhenPlaying]
		public float SampleInterval = 0.02f;
		[MMDropdown(2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192)]
		[MMReadOnlyWhenPlaying]
		public int SpectrumSamples = 1024;
		[MMReadOnlyWhenPlaying]
		public FFTWindow Window = FFTWindow.Rectangular;
		[Range(1, 64)]
		[MMReadOnlyWhenPlaying]
		public int NumberOfBands = 8;
		public float BufferSpeed = 2f;

		[Header("Beat Events")]
		public Beat[] Beats;

		[HideInInspector]
		public float[] RawSpectrum;

		[HideInInspector]
		public float[] BandLevels;
		[HideInInspector]
		public float[] BufferedBandLevels;

		[HideInInspector]
		public float[] BandPeaks;
		[HideInInspector]
		public float[] LastPeaksAt;

		[HideInInspector]
		public float[] NormalizedBandLevels;
		[HideInInspector]
		public float[] NormalizedBufferedBandLevels;

		[HideInInspector]
		public float Amplitude;
		[HideInInspector]
		public float NormalizedAmplitude;
		[HideInInspector]
		public float BufferedAmplitude;
		[HideInInspector]
		public float NormalizedBufferedAmplitude;
		[HideInInspector]
		public bool Active = false;
		[HideInInspector]
		public bool PeaksPasted = false;

		protected const int _microphoneDuration = 5;
		protected string _microphone;
		protected float _microphoneStartedAt = 0f;
		protected const float _microphoneDelay = 0.030f;
		protected const float _microphoneFrequency = 24000f;
		protected WaitForSeconds _sampleIntervalWaitForSeconds;
		protected int _cachedNumberOfBands;

		public virtual void FindPeaks()
		{
			float time = 0f;
			while (time < TargetAudioSource.clip.length)
			{
				TargetAudioSource.time = time;
				TargetAudioSource.GetSpectrumData(RawSpectrum, 0, Window);
				time += SampleInterval;
				ComputeBandLevels();
				PeaksSaver.Peaks = BandPeaks;
			}
		}

		public virtual void PastePeaks()
		{
			BandPeaks = PeaksSaver.Peaks;
			PeaksSaver.Peaks = null;
			PeaksPasted = true;
		}

		public virtual void ClearPeaks()
		{
			BandPeaks = null;
			PeaksSaver.Peaks = null;
			PeaksPasted = false;
		}

		protected virtual void Awake()
		{
			Initialization();
		}

		public virtual void Initialization()
		{
			_cachedNumberOfBands = NumberOfBands;
			RawSpectrum = new float[SpectrumSamples];
			BandLevels = new float[_cachedNumberOfBands];
			BufferedBandLevels = new float[_cachedNumberOfBands];

			// we make sure our peaks match our bands
			if ((BandPeaks == null) || (BandPeaks.Length == 0))
			{
				BandPeaks = new float[_cachedNumberOfBands];
				PeaksPasted = false;
			}
			if (BandPeaks.Length != BandLevels.Length)
			{
				BandPeaks = new float[_cachedNumberOfBands];
				PeaksPasted = false;
			}
			LastPeaksAt = new float[_cachedNumberOfBands];
			NormalizedBandLevels = new float[_cachedNumberOfBands];
			NormalizedBufferedBandLevels = new float[_cachedNumberOfBands];

			if ((Mode == Modes.AudioSource) && (TargetAudioSource == null))
			{
				Debug.LogError(this.name + " : this MMAudioAnalyzer needs a target audio source to operate.");
				return;
			}

			if (Mode == Modes.Microphone)
			{
				#if !UNITY_WEBGL
				GameObject audioSourceGo = new GameObject("Microphone");
				SceneManager.MoveGameObjectToScene(audioSourceGo, this.gameObject.scene);
				audioSourceGo.transform.SetParent(this.gameObject.transform);
				TargetAudioSource = audioSourceGo.AddComponent<AudioSource>();                
				//UNCOMMENT_MICROPHONE string _microphone = Microphone.devices[MicrophoneID].ToString();
				//UNCOMMENT_MICROPHONE TargetAudioSource.clip = Microphone.Start(_microphone, true, _microphoneDuration, (int)_microphoneFrequency);
				//UNCOMMENT_MICROPHONE TargetAudioSource.Play();
				_microphoneStartedAt = Time.time;
				#endif
			}

			Active = true;
			_sampleIntervalWaitForSeconds = new WaitForSeconds(SampleInterval);
			StartCoroutine(Analyze());
		}

		protected virtual void Update()
		{
			HandleBuffer();
			ComputeAmplitudes();
			HandleBeats();
		}

		protected virtual IEnumerator Analyze()
		{
			while (true)
			{
				switch (Mode)
				{
					case Modes.AudioSource:
						TargetAudioSource.GetSpectrumData(RawSpectrum, 0, Window);
						break;
					case Modes.Global:
						AudioListener.GetSpectrumData(RawSpectrum, 0, Window);
						break;
					case Modes.Microphone:
						#if !UNITY_WEBGL
						int microphoneSamples = 0;
						//UNCOMMENT_MICROPHONE microphoneSamples = Microphone.GetPosition(_microphone);
						if (microphoneSamples / _microphoneFrequency > _microphoneDelay)
						{
							if (!TargetAudioSource.isPlaying)
							{
								TargetAudioSource.timeSamples = (int)(microphoneSamples - (_microphoneDelay * _microphoneFrequency));
								TargetAudioSource.Play();
							}
							_microphoneStartedAt = Time.time;
						}
						AudioListener.GetSpectrumData(RawSpectrum, 0, Window);
						#endif
						break;
				}

				ComputeBandLevels();
				yield return _sampleIntervalWaitForSeconds;
			}
		}

		protected virtual void HandleBuffer()
		{
			for (int i = 0; i < BandLevels.Length; i++)
			{
				BufferedBandLevels[i] = Mathf.Max(BufferedBandLevels[i] * Mathf.Exp(-BufferSpeed * Time.deltaTime), BandLevels[i]);

				NormalizedBandLevels[i] = BandLevels[i] / BandPeaks[i];
				NormalizedBufferedBandLevels[i] = BufferedBandLevels[i] / BandPeaks[i];
			}
		}

		protected virtual void ComputeBandLevels()
		{
			float coefficient = Mathf.Log(RawSpectrum.Length);
			int offset = 0;
			for (int i = 0; i < BandLevels.Length; i++)
			{
				float savedSum = 0f;
				float next = Mathf.Exp(coefficient / BandLevels.Length * (i + 1));
				float weight = 1f / (next - offset);
				for (float sum = 0f; offset < next; offset++)
				{
					sum += RawSpectrum[offset];
					savedSum = sum;
				}
				BandLevels[i] = Mathf.Sqrt(weight * savedSum);
				if (BandLevels[i] > BandPeaks[i])
				{
					BandPeaks[i] = BandLevels[i];
					LastPeaksAt[i] = Time.time;
				}
			}
		}

		protected virtual void ComputeAmplitudes()
		{
			Amplitude = 0f;
			BufferedAmplitude = 0f;
			NormalizedAmplitude = 0f;
			NormalizedBufferedAmplitude = 0f;
			for (int i = 0; i < _cachedNumberOfBands; i++)
			{
				Amplitude += BandLevels[i];
				BufferedAmplitude += BufferedBandLevels[i];
				NormalizedAmplitude += NormalizedBandLevels[i];
				NormalizedBufferedAmplitude += NormalizedBufferedBandLevels[i];
			}
			Amplitude = Amplitude / _cachedNumberOfBands;
			BufferedAmplitude = BufferedAmplitude / _cachedNumberOfBands;
			NormalizedAmplitude = NormalizedAmplitude / _cachedNumberOfBands;
			NormalizedBufferedAmplitude = NormalizedBufferedAmplitude / _cachedNumberOfBands;
		}

		protected virtual void HandleBeats()
		{
			if (Beats.Length <= 0)
			{
				return;
			}

			foreach (Beat beat in Beats)
			{
				float value = 0f;
				beat.BeatThisFrame = false;
				switch (beat.Mode)
				{
					case Beat.Modes.Amplitude:
						value = Amplitude;
						break;
					case Beat.Modes.AmplitudeBuffered:
						value = BufferedAmplitude;
						break;
					case Beat.Modes.BufferedNormalized:
						value = NormalizedBufferedBandLevels[beat.BandID];
						break;
					case Beat.Modes.BufferedRaw:
						value = BufferedBandLevels[beat.BandID];
						break;
					case Beat.Modes.Normalized:
						value = NormalizedBandLevels[beat.BandID];
						break;
					case Beat.Modes.NormalizedAmplitude:
						value = NormalizedAmplitude;
						break;
					case Beat.Modes.NormalizedAmplitudeBuffered:
						value = NormalizedBufferedAmplitude;
						break;
					case Beat.Modes.Raw:
						value = BandLevels[beat.BandID];
						break;
				}

				if (beat.BeatValueMode == Beat.BeatValueModes.Live)
				{
					beat.CurrentValue = value;
				}
				else
				{
					// if audio value went below the bias during this frame
					if ((beat._previousValue > beat.Threshold) && (value <= beat.Threshold))
					{
						// if minimum beat interval is reached
						if (Time.time - beat._lastBeatAt > beat.MinimumTimeBetweenBeats)
						{
							OnBeat(beat, value);
						}
					}

					// if audio value went above the bias during this frame
					if ((beat._previousValue <= beat.Threshold) && (value > beat.Threshold))
					{
						// if minimum beat interval is reached
						if (Time.time - beat._lastBeatAt > beat.MinimumTimeBetweenBeats)
						{
							OnBeat(beat, value);
						}
					}

					beat._previousValue = value;
				}
			}
		}

		protected virtual void OnBeat(Beat beat, float rawValue)
		{
			beat._lastBeatAt = Time.time;
			beat.BeatThisFrame = true;
			if (beat.OnBeat != null)
			{
				beat.OnBeat.Invoke();
			}
			MMBeatEvent.Trigger(beat.Name, beat.CurrentValue);
			StartCoroutine(RemapBeat(beat));
		}

		protected virtual IEnumerator RemapBeat(Beat beat)
		{
			float remapStartedAt = Time.time;

			while (Time.time - remapStartedAt < beat.RemappedAttack + beat.RemappedDecay)
			{
				// attack
				if (Time.time - remapStartedAt < beat.RemappedAttack)
				{
					beat.CurrentValue = Mathf.Lerp(0f, 1f, (Time.time - remapStartedAt) / beat.RemappedAttack);
				}
				if (Time.time - remapStartedAt > beat.RemappedAttack)
				{
					beat.CurrentValue = Mathf.Lerp(1f, 0f, (Time.time - remapStartedAt - beat.RemappedAttack) / beat.RemappedDecay);
				}
				yield return null;
			}
			beat.CurrentValue = 0f;
			yield break;
		}

		protected virtual void OnValidate()
		{
			if ((Beats == null) || (Beats.Length == 0))
			{
				return;
			}

			int bandCounter = 0;
			for (int i = 0; i < Beats.Length; i++)
			{
				if (bandCounter >= _cachedNumberOfBands)
				{
					bandCounter = 0;
				}
				Beats[i].InitializeIfNeeded(i, bandCounter);
				bandCounter++;
			}
		}
	}
}