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

// Sound manager
//
// Usage:
// Place sound files in /Resources/audio/  (can use subfolders too if required)
// Register sounds with this class as follows:
//     Sounds.RegisterMusic("music/intro_music", 0.5f, true);   // Registers a looping music track, with a levelling volume of 0.5
//     Sounds.Register("bang1", 1.0f);
//     Sounds.Register("bang2", 1.0f);
//     Sounds.Register("bang3", 1.0f);
//
// Register groups as required. Sounds may be in more than one group, and the auto-register methods save you lots of typing!
//     Sounds.AutoRegisterGroup("bang");
//
// Play a random sound from the group, or a specific sound:
//     Sounds.Play("intro_music");
//     Sounds.Play("bang");
//     Sounds.Stop("voice");
//
namespace com.deeperbeige.lib
{
    public static class Sounds
    {
		// Can listen through the Events class for these
		public const string EVENT_VOLUME_CHANGED = "volume_changed"; // Fired when global volume is changed (audio or music)
		public const string EVENT_MUTE_CHANGED   = "mute_changed";   // Fired when global mute is changed (audio or music, enabled or disabled)

		// Properties
		private static GameObject                     listener;           // Reference to scene's listener object (updated automatically)
		private static GameObject                     audioContainer;     // Reference to scene's AudioRuntime gameobject (created/updated automatically)
		private static bool                           initialised = false;
		private static Dictionary<string, SoundInfo>  sounds;             // All registered sounds
		private static List<SoundInfo>                playing;            // All currently playing sounds (clones of registered sound objects)
		private static Dictionary<string, SoundGroup> groups;             // All sound groups
		
		// Stores info about a registered sound, or a playing sound
		class SoundInfo
		{
			// Properties for registered sounds
			public string           id;                // ID for this sound
			public string           assetPath;         // Path within resources for this AudioClip
			public bool             isMusic;           // Is this sound a piece of music? (setting looping can modify behaviour between stings and loops)
			public bool             loop;              // Should this sound be looped automatically?
			public bool             stopOnSceneChange; // Should this sound be stopped when a scene-change event is detected?
			public float            levelingVolume;    // Multiplier to level the volume of this sound out against others. Nominally 0..1, but can be higher for very quiet sounds
			public AudioClip        clip;              // Reference to the audio clip to be played
			public int              usageCount;        // How many times has this sound been played?
			public List<SoundGroup> inGroups;          // Groups this sound is a member of (helps with checking restrictions)

			// Properties for playing sounds
			public Action     onComplete;              // Method to call when the sound is finished playing
			public GameObject gameObject;              // Dynamically created gameobject that's playing this sound
			public float      playingVolume;           // Volume of playing sound (not premultiplied with leveling or global volumes)

			public SoundInfo Clone()
			{
				SoundInfo result = new SoundInfo();

				result.id                = id;
				result.assetPath         = assetPath;
				result.isMusic           = isMusic;
				result.loop              = loop;
				result.stopOnSceneChange = stopOnSceneChange;
				result.levelingVolume    = levelingVolume;
				result.clip              = clip;
				result.usageCount        = usageCount;
				result.inGroups          = inGroups;

				result.onComplete        = onComplete;
				result.gameObject        = gameObject;
				result.playingVolume     = playingVolume;

				return result;
			}
		}

		// Defines what to do if a group sound is played while a member is already playing
		public enum GroupSelfCutMode
		{
			alwaysPlay,        // Always plays sound, even if another member of this group is playing
			ignoreIfPlaying,   // Silently fails to play requested sound if another member of this group is already playing (good for unimportant voice over)
			cutIfPlaying       // Cuts any playing member of this group if another member is played (good for important voice over)
		}

		// Stores info about a registered sound group
		class SoundGroup
		{
			public List<string>     IDs         = new List<string>();
			public GroupSelfCutMode selfCutMode = GroupSelfCutMode.alwaysPlay;
		}

		// Initialise (called from other methods as required)
		private static void Init()
		{
			initialised = true;

			if (! PlayerPrefs.HasKey("audioMute"))   PlayerPrefs.SetInt  ("audioMute",   0);
			if (! PlayerPrefs.HasKey("musicMute"))   PlayerPrefs.SetInt  ("musicMute",   0);
			if (! PlayerPrefs.HasKey("audioVolume")) PlayerPrefs.SetFloat("audioVolume", 1);
			if (! PlayerPrefs.HasKey("musicVolume")) PlayerPrefs.SetFloat("musicVolume", 1);
			
			_audioMute   = (PlayerPrefs.GetInt("audioMute") == 0) ? false : true;
			_musicMute   = (PlayerPrefs.GetInt("musicMute") == 0) ? false : true;
			_audioVolume = PlayerPrefs.GetFloat("audioVolume");
			_musicVolume = PlayerPrefs.GetFloat("musicVolume");
			
			sounds  = new Dictionary<string, SoundInfo>();
			playing = new List<SoundInfo>();
			groups  = new Dictionary<string, SoundGroup>();
			
			Events.Listen(Transition.EVENT_PRE_CHANGE,  SoundsOnSceneDestroy, false);
			Events.Listen(Transition.EVENT_POST_CHANGE, SoundsOnSceneChanged, false);
			Events.Listen(Events.EVENT_UPDATE,          SoundsOnUpdate,       false);
			Events.Listen(Sounds.EVENT_MUTE_CHANGED,    SoundsOnVolumeChange, false);
			Events.Listen(Sounds.EVENT_VOLUME_CHANGED,  SoundsOnVolumeChange, false);
			SoundsOnSceneChanged();
		}
		
		// Handler for volume or mute changing
		private static void SoundsOnVolumeChange()
		{
			foreach (SoundInfo info in playing)
			{
				info.gameObject.audio.volume = GetVolumeForPlayingSound(info);
			}
		}
		
		// Sounds.audioMute = true/false
		// Stored in playerprefs
		private static bool _audioMute;
        public  static bool audioMute
        {
			get { if (! initialised) Init(); return _audioMute; }
			set { if (! initialised) Init(); _audioMute = value; PlayerPrefs.SetInt("audioMute", _audioMute ? 1 : 0); Events.Send(EVENT_MUTE_CHANGED); }
        }

		// Sounds.musicMute = true/false
		// Stored in playerprefs
        private static bool _musicMute;
        public  static bool musicMute
        {
			get { if (! initialised) Init(); return _musicMute; }
			set { if (! initialised) Init(); _musicMute = value; PlayerPrefs.SetInt("musicMute", _musicMute ? 1 : 0); Events.Send(EVENT_MUTE_CHANGED); }
        }

		// Sounds.audioVolume = 0..1
		// Stored in playerprefs
		private static float _audioVolume = 1.0f;
        public  static float audioVolume
        {
			get { if (! initialised) Init(); return _audioVolume; }
			set { if (! initialised) Init(); _audioVolume = value; PlayerPrefs.SetFloat("audioVolume", _audioVolume); Events.Send(EVENT_VOLUME_CHANGED); }
        }

		// Sounds.musicVolume = 0..1
		// Stored in playerprefs
		private static float _musicVolume = 1.0f;
        public  static float musicVolume
        {
			get { if (! initialised) Init(); return _musicVolume; }
			set { if (! initialised) Init(); _musicVolume = value; PlayerPrefs.SetFloat("musicVolume", _musicVolume); Events.Send(EVENT_VOLUME_CHANGED); }
        }
        
        // Sounds.paused = true | false;
        // For pausing/resuming sounds when the game is paused. Can also call PauseAll or ResumeAll.
		private static bool _paused = false;
		public  static bool paused
		{
			get { if (! initialised) Init (); return _paused; }
			set { if (! initialised) Init (); _paused = value; if (_paused) PauseAll(); else ResumeAll(); }
		}

		// Enter-frame
		private static void SoundsOnUpdate()
		{
			if (! initialised) Init();
			
			// Maintain playing list
			if (! _paused)
			{
				for (int j=0; j < playing.Count; j++)
				{
					if (playing[j].gameObject.audio.isPlaying) continue;
					
					playing.RemoveAt(j);
					j--;
				}
			}
		}

		// Disconnect while the level loads
		private static void SoundsOnSceneDestroy()
		{
			if (! initialised) Init();

			// Stop any sounds that are playing and marked to be destroyed
			foreach (SoundInfo info in playing)
			{
				if (! info.stopOnSceneChange) continue;
				
				info.gameObject.audio.Stop();
			}

			// Preserve the audiocontainer, but not its heirachy
			audioContainer.transform.parent = null;
			UnityEngine.Object.DontDestroyOnLoad(audioContainer);
		}

		// Reconnect to new listener on scene change
		// Can be called twice in the same scene without harm
		private static void SoundsOnSceneChanged()
		{
			if (! initialised) Init();

			AudioListener audioListener = UnityEngine.Object.FindObjectOfType<AudioListener>();
			if (audioListener == null)
			{
				DevPanel.Log("Sounds: Could not find audio listener in scene " + Application.loadedLevelName);
				return;
			}

			listener = audioListener.gameObject;
			audioContainer = GameObjects.FindAnywhere("AudioRuntime", true);
			if (audioContainer == null) audioContainer = new GameObject("AudioRuntime");
			audioContainer.transform.parent            = listener.transform;
			audioContainer.transform.localPosition     = Vector3.zero;
		}

		// Expects sounds to be in Assets/Resources/audio
		// Can use sub-folders if needed, they will simply become part of the ID
		// E.g. "voice/intro" refers to Assets/Resources/audio/voice/intro.wav, and the ID for this sound will become "intro"
		public static void Register(string id) { Register(id, 1.0f); }
		public static void Register(string id, float volumeAdjust) { Register(id, volumeAdjust, false, false, true); }
		public static void Register(string id, float volumeAdjust, bool isMusic, bool isLooping, bool stopOnSceneChange)
		{
			if (! initialised) Init();

			string assetPath = "Audio/" + id;
			int pos = assetPath.LastIndexOf("/");
			id = assetPath.Substring(pos + 1);

			if (string.IsNullOrEmpty(id))
			{
				DevPanel.Log("Sounds: Cannot register sound with null/empty ID");
				return;
			}
			
			if (SoundExists(id))
			{
				DevPanel.Log("Sounds: Trying to register sound '" + id + "' but it already exists");
				return;
			}

			if (GroupExists(id))
			{
				DevPanel.Log("Sounds: Trying to register sound '" + id + "' but a group already exists with this name");
				return;
			}

			SoundInfo info = new SoundInfo();
			info.id                = id;
			info.assetPath         = assetPath;
			info.levelingVolume    = volumeAdjust;
			info.isMusic           = isMusic;
			info.loop              = isLooping;
			info.stopOnSceneChange = stopOnSceneChange;
			info.usageCount        = 0;
			info.inGroups          = new List<SoundGroup>();

			// Check the clip exists in Resources/audio
			info.clip = Resources.Load(info.assetPath) as AudioClip;
			if (info.clip == null)
			{
				Debug.Log("Sounds: Trying to register sound '" + id + "' but resource not found at 'Resources/audio/" + id + "'");
			}

			// This unloads any sounds that are meant to stream from disk, freeing memory
			// They automatically reload themselves when required, incredibly!
			// See forum thread here:
			// http://forum.unity3d.com/threads/162779-Loading-Streaming-Audio-from-the-Resources-Folder
			Resources.UnloadAsset(info.clip);

			// Add sound to list of registered sounds
			sounds.Add(id, info);
		}

		// Register music
		public static void RegisterMusic(string id, float volumeAdjust, bool isLooping) { Register(id, volumeAdjust, true, isLooping, false); }

		// Register a group of sounds
		public static void RegisterGroup(string id, List<string> members) { RegisterGroup(id, members, GroupSelfCutMode.alwaysPlay); }
		public static void RegisterGroup(string id, List<string> members, GroupSelfCutMode selfCutMode)
		{
			if (! initialised) Init();

			// Check parameters
			if (string.IsNullOrEmpty(id))
			{
				DevPanel.Log("Sounds: Cannot register group with null/empty ID");
				return;
			}

			if (GroupExists(id))
			{
				DevPanel.Log("Sounds: Trying to register existing group '" + id + "'");
				return;
			}
			
			if (SoundExists(id))
			{
				DevPanel.Log("Sounds: Trying to register group '" + id + "' but a sound with that name already exists");
				return;
			}

			if (members.Count == 0)
			{
				DevPanel.Log("Sounds: Trying to register group '" + id + "' with no members");
				return;
			}

			// Create group
			SoundGroup group  = new SoundGroup();
			group.IDs         = members;
			group.selfCutMode = selfCutMode;

			// Add to groups list
			groups.Add(id, group);
			
			// Update inGroups for each member sound
			foreach (string soundID in members)
			{
				SoundInfo info = sounds[soundID];
				info.inGroups.Add(group);
			}
		}

		// Register a group of sounds based on registered names
		// Eg, if you have registered sounds as follows:
		//       bang-loud
		//       bang-medium
		//       bang-quiet
		//
		// You can collect them all into a group called 'explosions' with
		//       Sounds.AutoRegisterGroup("explosions", "bang-");
		//
		// Or a group called 'bang' with
		//       Sounds.AutoRegisterGroup("bang");
		//
		public static void AutoRegisterGroup(string idAndStartingWith)                               { AutoRegisterGroup(idAndStartingWith, GroupSelfCutMode.alwaysPlay); }
		public static void AutoRegisterGroup(string id, string startingWith)                         { AutoRegisterGroup(id, startingWith, GroupSelfCutMode.alwaysPlay); }
		public static void AutoRegisterGroup(string idAndStartingWith, GroupSelfCutMode selfCutMode) { AutoRegisterGroup(idAndStartingWith, idAndStartingWith); }
		public static void AutoRegisterGroup(string id, string startingWith, GroupSelfCutMode selfCutMode)
		{
			if (! initialised) Init();

			List<string> members = new List<string>();
			foreach (string soundID in sounds.Keys)
			{
				if (soundID.StartsWith(startingWith)) members.Add(soundID);
			}

			if (members.Count == 0)
			{
				DevPanel.Log("Sounds: No members found for auto-registered group '" + id + "'");
				return;
			}

			RegisterGroup(id, members, selfCutMode);
		}

		// Create a new group from the sounds found in a series of groups
		// Eg, if you have defined voice groups "hello" and "goodbye", it might
		// make sense to add a group of all voices like this:
		//       Sounds.RegisterGroupFromGroups("voice", new List<string>() { "hello", "goodbye" })
		public static void RegisterGroupFromGroups(string id, List<string> groupIDs) { RegisterGroupFromGroups(id, groupIDs, GroupSelfCutMode.alwaysPlay); }
		public static void RegisterGroupFromGroups(string id, List<string> groupIDs, GroupSelfCutMode selfCutMode)
		{
			if (! initialised) Init();

			List<string> soundIDs = new List<string>();

			foreach (string groupID in groupIDs)
			{
				if (! GroupExists(groupID))
				{
					DevPanel.Log("Sounds: Cannot create group '" + id + "' from nonexistent group '" + groupID + "'");
					return;
				}

				soundIDs = Lists.Union(soundIDs, groups[groupID].IDs);
			}

			RegisterGroup(id, soundIDs, selfCutMode);
		}

		// Instantiates a new game object and plays a sound through it
		// Set volume from 0..1. Can overdrive if required too - multiplies with sound's own volume adjust and global volume
		// Set loop to true to force the sound to loop, or false to use default
		// Method onComplete is called on completion
		// If ignoreGroupRestrictions is true, will ignore any group play restrictions
		public static AudioSource Play(string id)                               { return Play(id, 1.0f); }
		public static AudioSource Play(string id, float volume)                 { return Play(id, volume, false); }
		public static AudioSource Play(string id, float volume, bool forceLoop) { return Play(id, volume, forceLoop, null, false); }
		public static AudioSource Play(string id, float volume, bool forceLoop, Action onComplete, bool ignoreGroupRestrictions)
		{
			if (! initialised) Init();
			if (_paused) return null;

			string playID = id;

			// Pick from group if required
			if (GroupExists(id))
			{
				playID = Lists.RandomElement(groups[id].IDs);
			}

			// Check sound exists
			if (! SoundExists(playID))
			{
				DevPanel.Log("Sounds: Trying to play unregistered sound '" + playID + "'");
				return null;
			}

			// Update registered sound info
			SoundInfo defaultInfo = sounds[playID];
			defaultInfo.usageCount++;

			// Find sound details
			SoundInfo playingInfo = defaultInfo.Clone();

			// Check group restrictions
			bool cancel = false;
			foreach (SoundGroup group in playingInfo.inGroups)
			{
				switch (group.selfCutMode)
				{
					case GroupSelfCutMode.alwaysPlay:      break;
					case GroupSelfCutMode.cutIfPlaying:    foreach (string soundID in group.IDs) Sounds.Stop(soundID); break;
					case GroupSelfCutMode.ignoreIfPlaying: foreach (string soundID in group.IDs) if (IsPlaying(soundID)) cancel = true; break;
				}
			}
			if (cancel) return null;
			
			// Configure sound
			playingInfo.playingVolume       = volume;
			if (forceLoop) playingInfo.loop = true;
			playingInfo.onComplete          = onComplete;
			playing.Add(playingInfo);

			// Finally! Play sound
			playingInfo.gameObject = new GameObject(playingInfo.id, typeof(AudioSource));
			playingInfo.gameObject.transform.parent        = audioContainer.transform;
			playingInfo.gameObject.transform.localPosition = Vector3.zero;
			playingInfo.gameObject.audio.playOnAwake       = false;
			playingInfo.gameObject.audio.clip              = playingInfo.clip;
			playingInfo.gameObject.audio.volume            = GetVolumeForPlayingSound(playingInfo);
			playingInfo.gameObject.audio.loop              = playingInfo.loop;
			playingInfo.gameObject.audio.Play();

			return playingInfo.gameObject.audio;
		}

		// Multiply the various volumes together to get a final volume to play
		private static float GetVolumeForPlayingSound(SoundInfo info)
		{
			if (! initialised) Init();

			if (info.isMusic)
			{
				if (musicMute) return 0;
				return info.playingVolume * info.levelingVolume * musicVolume;
			} else {
				if (audioMute) return 0;
				return info.playingVolume * info.levelingVolume * audioVolume;
			}
		}

		// Stop all playing instances of sound ID or group
		public static void Stop(string id)
		{
			if (! initialised) Init();

			if (SoundExists(id))
			{
				foreach (SoundInfo info in playing)
				{
					if (info.id != id) continue;
					
					info.gameObject.audio.Stop();
				}
			}
			
			if (GroupExists(id))
			{
				SoundGroup group = groups[id];
				foreach (string soundID in group.IDs) Stop(soundID);
			}
		}

		// Set volume of any playing instances of a sound or group
		public static void Volume(string id, float volume)
		{
			if (! initialised) Init();
			
			if (SoundExists(id))
			{
				foreach (SoundInfo info in playing)
				{
					if (info.id != id) continue;
					
					info.playingVolume = volume;
					info.gameObject.audio.volume = GetVolumeForPlayingSound(info);
				}
			}
			
			if (GroupExists(id))
			{
				SoundGroup group = groups[id];
				foreach (string soundID in group.IDs) Volume(soundID, volume);
			}
		}
		
		// Pauses all playing sounds. Resume with ResumeAll()
		// Also locks out playing of new sounds. Calls to Play()
		// are silently ignored. Intended for game-pause routines
		public static void PauseAll()
		{
			if (! initialised) Init();
			
			_paused = true;
			foreach (SoundInfo info in playing)
			{
				info.gameObject.audio.Pause();
			}
		}

		// Reusmes all sounds paused with PauseAll()
		public static void ResumeAll()
		{
			if (! initialised) Init();

			_paused = false;
			foreach (SoundInfo info in playing)
			{
				info.gameObject.audio.Play();
			}
		}

		public static bool IsPlaying(string id)
		{
			if (! initialised) Init();

			// Check for sounds
			if (SoundExists(id))
			{
				foreach (SoundInfo info in playing)
				{
					if (info.id == id) return true;
				}

				return false;
			}

			// Check for groups
			if (GroupExists(id))
			{
				foreach (string soundID in groups[id].IDs)
				{
					if (IsPlaying (soundID)) return true;
				}
				
				return false;
			}

			// Not a known group or sound ID
			DevPanel.Log("Sounds: Trying to check IsPlaying for unknown ID '" + id + "'");
			return false;
		}

		// Returns true if a sound with this ID exists
		public static bool SoundExists(string id)
		{
			if (! initialised) Init();

			return sounds.ContainsKey(id);
		}
		
		// Returns true if a group with this ID exists
		public static bool GroupExists(string id)
		{
			if (! initialised) Init();

			return groups.ContainsKey(id);
		}

		// Inspect sounds and resources for simple errors - should be called
		// immediately after registering your sounds, and left in for production.
		// This routine will be silent when you've fixed everything.
		public static void ReportErrors()
		{
			if (! initialised) Init();

			AudioClip[] allSounds = Resources.LoadAll<AudioClip>("audio");
			List<AudioClip> unusedSounds = new List<AudioClip>();
			
			foreach (AudioClip sound in allSounds)
			{
				if (! SoundExists(sound.name)) unusedSounds.Add(sound);
			}
			
			if (unusedSounds.Count > 0)
			{
				DevPanel.Log("Sounds: Found " + unusedSounds.Count + " unregistered sounds in Resources:");
				foreach (AudioClip unusedSound in unusedSounds)
				{
					DevPanel.Log("  Unregistered: " + unusedSound.name);
				}
			}
		}

		// Trace out a usage report to help identify unused sounds etc
		// Intended for development use - can remove for production
		public static void ReportUsage()
		{
			if (! initialised) Init();

			DevPanel.Log("Sounds: Found " + sounds.Keys.Count + " registered sounds and " + groups.Keys.Count + " groups");
			
			SoundInfo[] IDs = new SoundInfo[sounds.Values.Count];
			sounds.Values.CopyTo(IDs, 0);
			Array.Sort<SoundInfo>(IDs, SortHelperSoundInfo);
			
			foreach (SoundInfo info in IDs)
			{
				DevPanel.Log(String.Format("  {0,4} plays: {1}", info.usageCount, info.id));
			}
		}
		
		private static int SortHelperSoundInfo(SoundInfo x, SoundInfo y)
		{
			if (x.usageCount < y.usageCount) return -1;
			if (x.usageCount > y.usageCount) return  1;
			return string.Compare(x.id, y.id);
		}
    }
}
