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

[ExecuteInEditMode]
public class SystemAudio : Singleton<SystemAudio>/*, ISaveable */
{
	// Definitions
	//////////////////////////////////////////////////////////////////////////////////// 
	
	static readonly string STRING_NAME_PREFIX = "Audio: ";

	[System.Serializable]
	class AudioTypeVolume
	{
		public AudioCue.eAudioType m_type = AudioCue.eAudioType.Sound;
		public float m_volume = 1.0f;
	};
	
	public class ClipInfo
    {
       //ClipInfo used to maintain default audio source info
       	public AudioSource source { get; set; }
		public int type { get; set; }
       	public float defaultVolume { get; set; }
		public float defaultPitch { get; set; }
		public Transform emmitter { get; set; }
		public Vector2 position { get; set; }
	}

	static List<AudioSource> s_audioSourceDefaultList = new List<AudioSource>();
	
	// Editable vars
	//////////////////////////////////////////////////////////////////////////////////// 
	
	[SerializeField] List<AudioTypeVolume> m_volumeByType = new List<AudioTypeVolume>();
	[Tooltip("Audio cues that can be played by name")]
	[SerializeField] List<AudioCue> m_audioCues = null;
	[SerializeField] float m_noDuplicateTime = 0.05f;
		
	
	// private vars
	//////////////////////////////////////////////////////////////////////////////////// 
 
    List<ClipInfo> m_activeAudio;
	AudioSource m_activeMusic;
		
	Transform m_audioListener = null;
	
	bool m_hasFocus = true;

	static readonly int AUDIO_SOURCE_POOL_SIZE = 8;
	List<AudioSource> m_audioSources = new List<AudioSource>();
	
	// Public functions
	//////////////////////////////////////////////////////////////////////////////////// 
	
	public static void SetVolume( AudioCue.eAudioType type, float volume )
	{
		SystemAudio self = Instance;
		float oldVolume = 1.0f;
		bool exists = false;
		
		
		if ( volume <= 0 ) // If volume goes to zero we lose the scale information. So make it -1 instead... mathyhack!
			volume = -1;
		
		for ( int i = 0; i < self.m_volumeByType.Count; ++i )
		{
			if ( self.m_volumeByType[i].m_type == type )
			{
				oldVolume = self.m_volumeByType[i].m_volume;
				self.m_volumeByType[i].m_volume = volume;
				exists = true;
				break;
			}
		}
		
		if ( exists == false )
		{
			// Doesn't exist yet, so add
			self.m_volumeByType.Add( new AudioTypeVolume() { m_type = type, m_volume = volume } );
		}

		float volChange = volume / oldVolume;
		// Update active audio volumes
		for ( int i = 0; i < self.m_activeAudio.Count; ++i )
		{
			ClipInfo info = self.m_activeAudio[i];
			if ( info != null &&  (info.type & (int)type) != 0 )
			{					
				if ( info.source != null )
					info.source.volume *= volChange;	
				info.defaultVolume *= volChange;
			}
		}
		
	}
	public static float GetVolume( AudioCue.eAudioType type )
	{
		SystemAudio self = Instance;
		for ( int i = 0; i < self.m_volumeByType.Count; ++i )
		{
			if ( self.m_volumeByType[i].m_type == type )
			{
				return Mathf.Clamp01(self.m_volumeByType[i].m_volume);
			}
		}
		return 1.0f;
	}

	// Play a cue in the AudioSystem based on string
	static public AudioSource Play( string cueName, Transform emmitter = null )
	{
		AudioCue cue = m_instance.m_audioCues.Find(item=>item.name == cueName);
		if ( cue == null && Debug.isDebugBuild )
		{
			Debug.LogWarning("Sound cue not found: "+cueName);
		}
		return Play( cue , emmitter );
	}
	
	static public AudioSource Play(AudioCue cue )
	{
		return Play( cue, null);
	}	

	static public AudioSource Play(AudioCue cue, ref List<AudioSource> sources )
	{
		return Play( cue, null, ref sources);
	}

	static public AudioSource Play(AudioCue cue, Transform emmitter )
	{
		return Play(cue, emmitter, ref s_audioSourceDefaultList );
	}

	static public AudioSource Play(AudioCue cue, Transform emmitter, ref List<AudioSource> sources )
	{
		if ( cue == null )
			return null;
		
		SystemAudio self = Instance;
		
		AudioCue.Clip cueClip = cue.GetClip();	
		
		if ( cueClip == null )
			return null;

		if ( Random.value > cue.m_chance )
			return null;
		
		AudioClip clip = cueClip.m_sound;
		
		if ( clip == null )
			return null;
		
		// Check if sound is already being played within X time
		for ( int i = 0; i < self.m_activeAudio.Count; ++i )
		{
			ClipInfo info = self.m_activeAudio[i];
			if ( info.source != null && info.source.clip == clip && info.source.time < self.m_noDuplicateTime )
			{
				//Debug.Log("AUD: duplicate"+info.source.name);
				return null;
			}
		}
		
		float volume = cue.m_volume.GetRandom() * cueClip.m_volume.GetRandom();
		
		// Apply type volumes
		for ( int i = 0; i < self.m_volumeByType.Count; ++i )
		{
			if ( ((int)self.m_volumeByType[i].m_type & cue.m_type) != 0 )
				volume *= self.m_volumeByType[i].m_volume;
		}
		
		float pitch =  cueClip.m_pitch.GetRandom();
		//float priority = cue.m_priority;
				
		// Create the source
		AudioSource source = self.SpawnAudioSource( 
		                                           ( Debug.isDebugBuild ) ? STRING_NAME_PREFIX + clip.name : STRING_NAME_PREFIX, // don't bother with strings if not debugging
		                                           self.transform.position);

		
		/* Use audiosystem as source of all sounds */
		source.transform.parent = self.transform;
			
		source.transform.localPosition = Vector3.zero;
		
		self.SetSource(ref source, clip, volume, pitch, cue.m_priority );		
		source.loop = cueClip.m_loop;
		
		// Play (with start delay if one exists)
		float startDelay = cue.m_startDelay.GetRandom();
		if ( startDelay > 0.0f )
			source.PlayDelayed(startDelay);
		else 
			source.Play();	

		if ( sources != s_audioSourceDefaultList )
			sources.Add(source);
		

		// Shake camera
		/*if ( self.m_camera != null && cueClip.m_shakeTime > 0 )
		{
			self.m_camera.SendMessage("Shake", cueClip.m_shakeTime, cueClip.m_shakeType );
		}*/

		// if no emmitter specified, emit from audio system
		if ( emmitter == null )
		{
			emmitter = self.transform;
		}

		//Set the source as active
		self.m_activeAudio.Add(new ClipInfo{
			source = source, 
			type = cue.m_type,
			defaultVolume = volume, 
			defaultPitch = pitch,
			emmitter = emmitter == null ? self.transform : emmitter,
			position = (Vector2)emmitter.transform.position });

		if ( cue.m_alsoPlay != null )
			Play( cue.m_alsoPlay, emmitter, ref sources );

		return source;
	}


	static public AudioSource Play( AudioClip clip, float volume = 1, float pitch = 1, bool loop = false, int type = (int)AudioCue.eAudioType.Sound, Transform emmitter = null )
	{
		if ( clip == null)
			return null;

		SystemAudio self = Instance;
	
		
		// Check if sound is already being played within X time
		for ( int i = 0; i < self.m_activeAudio.Count; ++i )
		{
			ClipInfo info = self.m_activeAudio[i];
			if ( info.source != null && info.source.clip == clip && info.source.time < self.m_noDuplicateTime )
			{
				//Debug.Log("AUD: duplicate"+info.source.name);
				return null;
			}
		}
				
		// Apply type volumes
		for ( int i = 0; i < self.m_volumeByType.Count; ++i )
		{
			if ( ((int)self.m_volumeByType[i].m_type & type) != 0 )
				volume *= self.m_volumeByType[i].m_volume;
		}
				
		// Create the source
		AudioSource source = self.SpawnAudioSource( 
		                                           ( Debug.isDebugBuild ) ? STRING_NAME_PREFIX + clip.name : STRING_NAME_PREFIX, // don't bother with strings if not debugging
		                                           self.transform.position);

		
		/* Use audiosystem as source of all sounds */
		source.transform.parent = self.transform;
			
		source.transform.localPosition = Vector3.zero;
		
		self.SetSource(ref source, clip, volume, pitch, 0 );		
		source.loop = loop;

		source.Play();

		// Shake camera
		/*if ( self.m_camera != null && cueClip.m_shakeTime > 0 )
		{
			self.m_camera.SendMessage("Shake", cueClip.m_shakeTime, cueClip.m_shakeType );
		}*/

		// if no emmitter specified, emit from audio system
		if ( emmitter == null )
		{
			emmitter = self.transform;
		}

		//Set the source as active
		self.m_activeAudio.Add(new ClipInfo{
			source = source, 
			type = type,
			defaultVolume = volume, 
			defaultPitch = pitch,
			emmitter = emmitter == null ? self.transform : emmitter,
			position = (Vector2)emmitter.transform.position });


		return source;
	}

	public static void Stop(AudioSource source, float overTime = 0)
	{
		Stop(ref source, overTime);
	}
	public static void Stop(ref AudioSource source, float overTime = 0)
	{
		if ( m_instance && source != null && source.isPlaying )
		{
			if ( overTime <= 0 )
			{
				source.Stop();			
				source.gameObject.SetActive(false);
			}
			else 
			{
				m_instance.StartCoroutine( m_instance.CoroutineStopFade(source,overTime) );
			}
		}
		source = null;
	}

	IEnumerator CoroutineStopFade(AudioSource source, float time)
	{		
		ClipInfo clipInfo = m_activeAudio.Find( clip=>clip.source == source );
		if ( clipInfo != null  && time > 0.0f )
		{						
			float volumeDelta = (1.0f/time)* clipInfo.defaultVolume;

			while ( time > 0 )
			{
				if ( source == null || source.isPlaying == false || clipInfo == null )
					yield break;

				time -= Time.deltaTime;
				clipInfo.defaultVolume = clipInfo.defaultVolume - Time.deltaTime * volumeDelta;
				yield return new WaitForEndOfFrame();
			}
		}
		Stop(source);
	}
	
	IEnumerator CoroutineFadeIn( AudioSource source, float time )
	{	
		
		ClipInfo clipInfo = m_activeAudio.Find( clip=>clip.source == source );
		if ( clipInfo == null || time <= 0 )
			yield break;	
		
		float volumeDelta = (1.0f/time)* clipInfo.defaultVolume;
		clipInfo.defaultVolume = 0;
		
		while ( time > 0 )
		{
			if ( source == null || source.isPlaying == false || clipInfo == null )
				yield break;
			
			time -= Time.deltaTime;
			clipInfo.defaultVolume = clipInfo.defaultVolume + Time.deltaTime * volumeDelta;
			yield return new WaitForEndOfFrame();
		}
	}

	// Play a cue in the audio system matching the name
	public static AudioSource PlayMusic( string name, float fadeTime = 0 ) { return PlayMusic(name, fadeTime, fadeTime); }
	public static AudioSource PlayMusic( string name, float fadeOutTime, float fadeInTime ) 
	{
		return PlayMusic( m_instance.m_audioCues.Find(item=>item.name == name), fadeOutTime, fadeInTime );
	}

	public static AudioSource PlayMusic( AudioCue cue, float fadeTime = 0 ) { return PlayMusic(cue, fadeTime,fadeTime); }
	public static AudioSource PlayMusic( AudioCue cue, float fadeOutTime, float fadeInTime ) 
	{
		StopMusic(fadeOutTime);
		m_instance.m_activeMusic = Play(cue);
		if ( fadeInTime > 0 )
			m_instance.StartCoroutine( m_instance.CoroutineFadeIn(m_instance.m_activeMusic, fadeInTime) );
		return m_instance.m_activeMusic;
	}

	public static void StopMusic(float fadeTime = 0)
	{
		Stop(Instance.m_activeMusic, fadeTime);
		Instance.m_activeMusic = null;		
	}
		
	
	
	// Implementing ISaveable
	//////////////////////////////////////////////////////////////////////
	/*
	public void Sync( SaveHelper save )
	{		
		float vol = GetVolume(AudioCue.eAudioType.Sound);
		if ( Mathf.Approximately(vol,0.0f) ) vol = -1; // Set to -1 instead of zero. zero means it's not in the save file yet.
		save.Sync("SoundVol", ref vol);
		if ( Mathf.Approximately(vol,0.0f) == false )
			SetVolume(AudioCue.eAudioType.Sound, Mathf.Clamp01(vol));
		
		vol = GetVolume(AudioCue.eAudioType.Music);
		if ( Mathf.Approximately(vol,0.0f) ) vol = -1;
		save.Sync("MusicVol", ref vol);
		if ( Mathf.Approximately(vol,0.0f) == false )
			SetVolume(AudioCue.eAudioType.Music, Mathf.Clamp01(vol));
	}
	*/
	public void PostSync( bool saved ) {}
	
	// Private functions
	//////////////////////////////////////////////////////////////////////////////////// 
	
	
	void OnLevelWasLoaded( int level )
	{
        try 
		{
			m_audioListener = ((AudioListener)GameObject.FindObjectOfType(typeof(AudioListener)) ).transform;
			
			// Hack - Get camera for cheeky 4p specific stuff
			//m_camera = m_audioListener.GetComponent<Camera>();//GetComponent<CameraMain>();
			
			// Stop all sound effects			
			foreach( ClipInfo info in m_activeAudio )
			{
				if ( (info.type & (int)AudioCue.eAudioType.Music) == 0 )
				{
					// Not music, so end it. Give it a nice fade...
					Stop( info.source, 0.1f);
				}
			
			}
			
        } 
		catch 
		{
			if ( Debug.isDebugBuild ) Debug.Log("Unable to find audio listener in scene");
        }
	}
	
	
    void Awake() 
	{
		// if ( Debug.isDebugBuild ) Debug.Log("Audio system Initializing");
		SetSingleton(true);
		Object.DontDestroyOnLoad(this);	

		for ( int i = 0; i < AUDIO_SOURCE_POOL_SIZE; ++i )
		{
			GameObject soundLoc = new GameObject(STRING_NAME_PREFIX);
			m_audioSources.Add( soundLoc.AddComponent<AudioSource>() );
			soundLoc.SetActive(false);
			soundLoc.transform.parent = transform;
		}

		
        try 
		{
			m_audioListener = ((AudioListener)GameObject.FindObjectOfType(typeof(AudioListener)) ).transform;
			
			// Hack - Get camera for cheeky 4p specific stuff
			//m_camera = m_audioListener.GetComponent<Camera>();//.GetComponent<came<CameraMain>();
        } 
		catch 
		{
			if ( Debug.isDebugBuild ) Debug.Log("Unable to find audio listener in scene");
        }
		
		
	    m_activeAudio = new List<ClipInfo>();
	    m_activeMusic = null;

	
	}
	
	void OnDestroy()
	{		
	}
	
	void OnApplicationFocus(bool hasFocus )
	{
		m_hasFocus = hasFocus;
	}
	
	// Update is called once per frame
	void Update () 
	{
		if ( m_audioListener == null )
		{
	        try 
			{
				m_audioListener = ((AudioListener)GameObject.FindObjectOfType(typeof(AudioListener)) ).transform;
				
				// Hack - Get camera for cheeky 4p specific stuff
				//m_camera = m_audioListener.GetComponent<Camera>();//.GetComponent<CameraMain>();	
			}	
			catch 
			{
				//if ( Debug.isDebugBuild ) Debug.Log("Unable to find audio listener in scene");
	        }
		}
		
		transform.position = m_audioListener.position;
    	UpdateActiveAudio();		
	
	}
	
	float GetFalloff( Vector2 soundPos )
	{
		return 1.0f;
		/*
		float xStart = 53;
		float yStart = 30;
		float xFalloff = 212;
		float yFalloff = 120;
		float xRatio = Utils.EaseCubic(1.0f - Mathf.Clamp01((Mathf.Abs(soundPos.x - transform.position.x) - xStart) / xFalloff) );
		float yRatio = Utils.EaseCubic(1.0f - Mathf.Clamp01((Mathf.Abs(soundPos.y - transform.position.y) - yStart) / yFalloff) );
		
		return Mathf.Min( xRatio, yRatio );
		*/
	}
	
	void SetSource(ref AudioSource source, AudioClip clip, float volume, float pitch, int priority) 
	{
		source.rolloffMode = AudioRolloffMode.Linear;
		source.dopplerLevel = 0.2f;
		source.minDistance = 0;
		source.maxDistance = 50;
		source.priority = priority;
		source.pitch = pitch;
		source.clip = clip;
		float volumeMod = 1.0f;
		if ( source.transform )
			volumeMod *= GetFalloff(source.transform.position);
		source.volume = volume * volumeMod;
		source.spatialBlend = 0;
	}
	
	
	void UpdateActiveAudio() 
	{ 
		// First check if the game has focus, since if not, the audio is potentially still active
		if ( m_hasFocus == false )
			return;
		
		List<ClipInfo> toRemove = new List<ClipInfo>();
	    try 
		{
	        foreach (ClipInfo audioClip in m_activeAudio) 
			{
	            if (!audioClip.source || (audioClip.source.isPlaying) == false ) 
				{
	                toRemove.Add(audioClip);
	            } 
				else 
				{
					float volumeMod = 1.0f;			
					if ( audioClip.emmitter != null )
					{
						audioClip.position = audioClip.emmitter.position;
					}
					volumeMod *= GetFalloff(audioClip.position);		
					
					audioClip.source.volume = audioClip.defaultVolume * volumeMod;
				}
	        }
	    } 
		catch 
		{
			if ( Debug.isDebugBuild ) Debug.Log("Error updating active audio clips");
			if ( m_activeAudio == null )
			{
				m_activeAudio = new List<ClipInfo>();
			}
	       return;
	    }
	    //cleanup
	    foreach (ClipInfo audioClip in toRemove) 
		{
			m_activeAudio.Remove(audioClip);
			if ( audioClip.source != null )
			{
				audioClip.source.Stop();
				if ( audioClip.source.gameObject != null )				
					audioClip.source.gameObject.SetActive(false);
			}
	    }
		
		// this dunnae really work, because the clip isn't destroyed ever because you can't use destroy  with a timer in edit mode
		if ( Application.isEditor && Application.isPlaying == false && m_activeAudio.Count <= 0  )
		{
			DestroyImmediate(gameObject);
		}

			
	}

	AudioSource SpawnAudioSource( string name, Vector2 position )
	{
		AudioSource source = null;
		for ( int i = 0; i < m_audioSources.Count; ++i )
		{
			source = m_audioSources[i];
			if ( source == null )
			{
				// source has been deleted, add new one in place
				GameObject soundLoc = new GameObject();			
				//Create the source
				source = soundLoc.AddComponent<AudioSource>();
				m_audioSources[i] = source;
				soundLoc.SetActive(false);
				soundLoc.transform.parent = transform;
				break;
			}
			else if ( source.gameObject.activeSelf == false )
			{
				break;
			}
			source = null;
		}

		if ( source == null )
		{
			// no pooled source found, add new source
			GameObject soundLoc = new GameObject(STRING_NAME_PREFIX);			
			//Create the source
			source = soundLoc.AddComponent<AudioSource>();
			m_audioSources.Add(source);
			soundLoc.SetActive(false);
			soundLoc.transform.parent = transform;
		}
		if ( source != null )
		{
			source.gameObject.SetActive(true);
			if ( Debug.isDebugBuild )
				source.gameObject.name = name;
			source.gameObject.transform.position = position;
		}
		else 
		{
			if ( Debug.isDebugBuild ) Debug.Log("Failed to spawn audio source");
		}
		return source;

	}

}
