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

/*	None	 				= -1,
	Drown,					// 0
	Coin,                   // 1
	Pick,                   // 2
	Drop,                   // 3
	Work,                   // 4
    SplashSmall,            // 5
    SplashBig,              // 6
    RippleTrail,            // 7
    SteamBurst,             // 8
    SeagulCall,             // 9
    Count					// 
*/

[System.Serializable]
public class SoundProto : System.Object
{
    public string m_name;
    public AudioClip[] m_clip;
	public int m_instancesCountMax = 1;
	[System.NonSerialized]
	public int m_instancesCount;
}

[System.Serializable]
public class MusicProto : System.Object
{
    public string m_name;
    public AudioClip m_clip;
    public float m_startTime;
    public float m_loopTime;

    public float Length { get { return m_clip.length; } }
}

public class SoundVoice
{
	public bool 		m_playing;
    public int          m_soundID;
	public AudioSource 	m_source;
    public float        m_volume; // sound volume
}

public class AudioManager : MonoBehaviourSingleton<AudioManager> 
{
    public enum Channel
    {
        SFX,
        Music,
        Ambient,
    }

    [Tooltip("The list of sounds")]
	public SoundProto[] m_soundProtos;

	[Tooltip("The list of musics")]
	public MusicProto[] m_musicProtos;

	private const int VoicesCount = 16;
	private SoundVoice[] m_voices;

    private const int ChannelsCount = 3;
    private float[] m_volumes;

    private int m_musicIdx = -1; // index in queue
	private bool m_musicLoop = false;
    private bool m_musicJustStarted = false;
    private bool m_musicShuffle = true;
	private int m_musicLastID = -1;
	private float m_musicFadeOut = 0.0f;
    private float m_musicFadeOutDuration = 0.5f;

    private int m_ambientId = -1; // index in protos

    private List<int> m_musicQueue;

	private AudioSource m_musicSource;
    private AudioSource m_ambientSource;

    private Dictionary<string, int> m_soundProtosDic;
    private Dictionary<string, int> m_musicProtosDic;

    public void OnInit()
	{
        InitDictionaries();
        InitVoices();

        m_volumes = new float[ChannelsCount];
        for (int i = 0; i < ChannelsCount; i++)
            m_volumes[i] = 1.0f;

        m_musicQueue = new List<int>();

        AudioSource[] asrc = GetComponents<AudioSource>();
        m_musicSource = asrc[0];
        m_ambientSource = asrc.Length > 1 ? asrc[1] : null;

        #if !RELEASE
        // @DBG @BUILD
        // LoadAudioFromDisc();
        #endif
    }

    void InitDictionaries()
    {
        m_soundProtosDic = new Dictionary<string, int>();
        m_musicProtosDic = new Dictionary<string, int>();
        for (int i = 0; i < m_soundProtos.Length; i++)
            m_soundProtosDic.Add(m_soundProtos[i].m_name, i);
        for (int i = 0; i < m_musicProtos.Length; i++)
            m_musicProtosDic.Add(m_musicProtos[i].m_name, i);
    }

    void InitVoices()
	{
		m_voices = new SoundVoice[VoicesCount];
		for(var i=0; i<VoicesCount; i++)
		{
			GameObject obj = new GameObject();
			obj.name = "voice"+i;
			obj.transform.parent = transform;
			AudioSource source = obj.AddComponent<AudioSource>();
			source.playOnAwake = false;

			SoundVoice v = new SoundVoice();
			v.m_playing = false;
			v.m_soundID = -1;
			v.m_source = source;
			m_voices[i] = v;
		}
	}

    public float GetVolume(Channel channel)
    {
        return m_volumes[(int)channel];
    }

    public void SetVolume(Channel channel, float volume)
    {
        m_volumes[(int)channel] = volume;
        if (channel == Channel.SFX)
        {
            foreach (var voice in m_voices)
            {
                if (voice.m_playing)
                    voice.m_source.volume = voice.m_volume * volume;
            }
        }
        else if (channel == Channel.Music)
        {
            m_musicSource.volume = volume;
        }
        else if (channel == Channel.Ambient)
        {
            if (m_ambientSource)
                m_ambientSource.volume = volume;
        }
    }

    // -----------------------------------------------------------------------------------------------------------------------
    // SOUND
    // -----------------------------------------------------------------------------------------------------------------------

    public bool IsSoundPlaying(string name)
    {
        int id;
        if (!m_soundProtosDic.TryGetValue(name, out id))
            return false;

        SoundProto sp = m_soundProtos[id];
        return sp.m_instancesCount > 0;
    }

    public int PlaySound2D(string name, Vector3 pos, float volume = 1.0f)
    {
        int id;
        if (!m_soundProtosDic.TryGetValue(name, out id))
            return -1;

        return PlaySound(id, volume, ComputePan(pos));
    }

    public int PlaySound(string name, float volume = 1.0f, float pan = 0.0f, bool loop = false)
    {
        int id;
        if (!m_soundProtosDic.TryGetValue(name, out id))
            return -1;

        return PlaySound(id, volume, pan, loop);
    }

    private int PlaySound(int id, float volume = 1.0f, float pan = 0.0f, bool loop = false)
    {
        SoundProto sp = m_soundProtos[id];
        if (sp.m_instancesCount < sp.m_instancesCountMax)
		{
			// find free voice
			for(int i=0; i<VoicesCount; i++)
			{
				SoundVoice v = m_voices[i];
				if(!v.m_playing)
				{
					v.m_playing = true;
					v.m_soundID = id;
                    v.m_volume = volume;
                    int clipidx = Random.Range(0, sp.m_clip.Length);
					v.m_source.clip = sp.m_clip[clipidx];
					v.m_source.volume = volume * m_volumes[(int)Channel.SFX];
					v.m_source.panStereo = pan;
					v.m_source.loop = loop;
					v.m_source.time = 0.0f;
					v.m_source.timeSamples = 0;
					v.m_source.Play();

					//Debug.Log(v.m_source.clip.name + " v=" + volume + " p=" + pan);

					sp.m_instancesCount++;
					return i;
				}
			}
		}

		return -1;
	}

    public void StopSound(string name)
    {
        int id;
        if (m_soundProtosDic.TryGetValue(name, out id))
        {
            SoundProto sp = m_soundProtos[id];
            if (sp.m_instancesCount > 0)
            {
                for (int i = 0; i < VoicesCount; i++)
                    if (m_voices[i].m_soundID == id)
                        StopVoice(i);
            }
        }
    }

    public static float ComputePan(Vector3 pos)
	{
		float pan = Vector3.Dot(Camera.main.transform.right, pos - Camera.main.transform.position);
		if(pan > 0.0f)
			return Mathf.Clamp((pan-1.5f) / 3.5f, 0.0f, 1.0f);
		else
			return Mathf.Clamp((pan+1.5f) / 3.5f, -1.0f, 0.0f);
	}

    public void StopAllVoices()
    {
        for (int i = 0; i < VoicesCount; i++)
            StopVoice(i);
    }

    public void StopVoice(int voice)
	{
		SoundVoice v = m_voices[voice];
		if(v.m_playing)
		{
            m_soundProtos[v.m_soundID].m_instancesCount--;
            v.m_playing = false;
            v.m_soundID = -1;
            v.m_source.Stop();
        }
	}

	// use with care, because other sound may have just been started on this voice
	public bool IsVoicePlaying(int voice)
	{
		SoundVoice v = m_voices[voice];
		return v.m_playing;
	}

	public void SetVoiceTime(int voice, float t)
	{
		SoundVoice v = m_voices[voice];
		if(v.m_playing)
			v.m_source.time = t;
	}

	public float GetVoiceTime(int voice)
	{
		SoundVoice v = m_voices[voice];
		return (v.m_playing) ? v.m_source.time : 0.0f;
	}

	public float GetVoiceLength(int voice)
	{
		SoundVoice v = m_voices[voice];
		if(v.m_playing)
			return v.m_source.clip.length;
		else
			return 0.0f;
	}

	public void SetVoiceVolume(int voice, float volume)
	{
		SoundVoice v = m_voices[voice];
        v.m_volume = volume;
		v.m_source.volume = volume;
	}

	public void SetVoicePos2D(int voice, Vector3 pos)
	{
		SoundVoice v = m_voices[voice];
		v.m_source.panStereo = ComputePan(pos);
	}

	public void SetVoicePan(int voice, float pan)
	{
		SoundVoice v = m_voices[voice];
		v.m_source.panStereo = pan;
	}

    // -----------------------------------------------------------------------------------------------------------------------
    // MUSIC
    // -----------------------------------------------------------------------------------------------------------------------

    public MusicProto GetMusicProto(string name)
    {
        int id;
        if (m_musicProtosDic.TryGetValue(name, out id))
            return m_musicProtos[id];
        else
            return null;
    }

    public bool IsPlayingMusic()
	{
		return 0 <= m_musicIdx && m_musicIdx < m_musicQueue.Count;
	}

    public string GetPlayingMusic()
    {
        int id = IsPlayingMusic() ? m_musicQueue[m_musicIdx] : -1;
        return id != -1 ? m_musicProtos[id].m_name : "";
    }

    public float GetMusicTime()
    {
        return IsPlayingMusic() ? m_musicSource.time : 0.0f;
    }

    public float GetMusicLength()
    {
        return IsPlayingMusic() ? m_musicSource.clip.length : 0.0f;
    }

    private void PlayMusic()
	{
		int id = m_musicQueue[m_musicIdx];
        // Debug.Log("play m "+id);
        MusicProto musicProto = m_musicProtos[id];
        m_musicSource.clip = musicProto.m_clip;
        m_musicSource.time = musicProto.m_startTime;
        m_musicSource.volume = m_volumes[(int)Channel.Music];
        m_musicSource.loop = false;
        m_musicSource.Play();
		m_musicLastID = id;
        musicProto.m_startTime = musicProto.m_loopTime;

        m_musicJustStarted = false;
    }

    public void PlayMusic(string name, bool loop, float fadeOutDuration = 0)
	{
        int id;
        if (!m_musicProtosDic.TryGetValue(name, out id))
            return;

        StopMusic(fadeOutDuration);

		m_musicQueue.Add(id);

        // @NOTE this is not good for non loop i think >> fixed this, no more real loop; now it's supported with m_musicJustStarted
        // Debug.Assert(loop);

        m_musicIdx = 0;
		m_musicLoop = loop;
        m_musicJustStarted = true;
    }

    /* @REM obsolete
	public void PlayMusicAll(bool loop)
	{
		StopMusic();

		for(int i=0; i< m_musicProtos.Length; i++)
			m_musicQueue.Add(i);

		ShuffleMusicQueue();

        // @NOTE this is not good
		m_musicIdx = 0;
		m_musicLoop = loop;
	}
    */

	public void StopMusic(float fadeOutDuration = 0.0f)
	{
		m_musicIdx = -1;
		m_musicQueue.Clear();
        if (fadeOutDuration == 0)
        {
            m_musicFadeOut = 0.0f;
            m_musicSource.Stop();
        }
        else
        {
            m_musicFadeOut = 1.0f;
            m_musicFadeOutDuration = fadeOutDuration;
        }
    }

	private void ShuffleMusicQueue()
	{
		List<int> musics = new List<int>();
		for(int i=0; i<m_musicQueue.Count; i++)
		{
			musics.Add(m_musicQueue[i]);
		}

		for(int i=0; i<m_musicQueue.Count; i++)
		{
			int r = Random.Range(0, musics.Count);
			m_musicQueue[i] = musics[r];
			musics.RemoveAt(r);
		}

		// avoid playing previous music ID first
		if(m_musicLastID != -1)
		{
			if(m_musicQueue.Count > 1 && m_musicQueue[0] == m_musicLastID)
			{
				int r = Random.Range(1, m_musicQueue.Count);
				m_musicQueue[0] = m_musicQueue[r];
				m_musicQueue[r] = m_musicLastID;
			}
		}
	}

	public void OnUpdate()
	{
		UpdateVoices();
		UpdateMusic();
	}

	private void UpdateVoices()
	{
		for(var i=0; i<VoicesCount; i++)
		{
			SoundVoice v = m_voices[i];
			if(v.m_playing)
			{
				if(!v.m_source.isPlaying)
				{
					v.m_playing = false;
					if(v.m_soundID != -1)
					{
						m_soundProtos[v.m_soundID].m_instancesCount--;
						v.m_soundID = -1;
					}
				}
			}
		}
	}

	private void UpdateMusic()
	{
		// handle fadeout request first, before starting to play
		if(m_musicFadeOut > 0.0f)
		{
			if(m_musicSource.isPlaying)
			{
				m_musicFadeOut -= Time.deltaTime / m_musicFadeOutDuration;
				if(m_musicFadeOut <= 0.0f)
				{
					m_musicSource.Stop();
					m_musicFadeOut = 0.0f;
				}
				else
				{
					m_musicSource.volume = m_volumes[(int)Channel.Music] * m_musicFadeOut;
				}
			}
			else
			{
				m_musicFadeOut = 0.0f;
			}

			// if done fading, start the queue
			if(m_musicFadeOut == 0.0f && IsPlayingMusic())
			{
				PlayMusic();
			}
		}
		else
		{
			if(IsPlayingMusic())
			{
				// when the current music has finished
				if(!m_musicSource.isPlaying)
				{
                    if(!m_musicJustStarted)
    					m_musicIdx++;

                    // check end of queue
                    if (m_musicIdx >= m_musicQueue.Count)
					{
						if(m_musicLoop)
						{
							m_musicIdx = 0;

							if(m_musicShuffle)
								ShuffleMusicQueue();

							PlayMusic();
						}
						else
						{
							// no more music
							m_musicQueue.Clear();
							m_musicIdx = -1;
						}
					}
					else
					{
						// play next song
						PlayMusic();
					}
				}
			}
		}
	}

    // -----------------------------------------------------------------------------------------------------------------------
    // AMBIENT
    // -----------------------------------------------------------------------------------------------------------------------

    public void PlayAmbient(string name)
    {
        int id;
        if (!m_musicProtosDic.TryGetValue(name, out id))
            return;

        m_ambientSource.Stop();

        m_ambientId = id;

        MusicProto proto = m_musicProtos[m_ambientId];
        m_ambientSource.clip = proto.m_clip;
        m_ambientSource.volume = m_volumes[(int)Channel.Ambient];
        m_ambientSource.loop = true;
        m_ambientSource.Play();
    }

    public void StopAmbient()
    {
        m_ambientSource.Stop();
        m_ambientId = -1;
    }

    public bool IsAmbientPlaying()
    {
        return m_ambientSource.isPlaying;
    }

    public string GetPlayingAmbient()
    {
        return IsAmbientPlaying() ? m_musicProtos[m_ambientId].m_name : "";
    }

    // -----------------------------------------------------------------------------------------------------------------------
    // RELOAD
    // -----------------------------------------------------------------------------------------------------------------------

    public void LoadAudioFromDisc()
    {
        StartCoroutine("LoadAudioFromDiscCo");
    }

    IEnumerator LoadAudioFromDiscCo()
    {
        App.instance.m_busy++;

        Debug.Log("Loading audio start");

        string playingMusic = GetPlayingMusic();
        string playingAmbient = GetPlayingAmbient();

        StopMusic();
        StopAmbient();
        StopAllVoices();

        // music and ambient
        var folder = new DirectoryInfo("Audio/Music");
        if (folder.Exists)
        {
            foreach (var proto in m_musicProtos)
            {
                var fileList = folder.GetFiles(proto.m_name + ".ogg");
                AddArray(ref fileList, folder.GetFiles(proto.m_name + "*.wav"));
                if (fileList.Length > 0)
                {
                    var file = fileList[0];
                    Debug.Log("  " + file.Name);

                    string path = file.FullName;

                    var www = new WWW("file://" + file.FullName);
                    var clip = www.GetAudioClipCompressed();

                    while (clip.loadState != AudioDataLoadState.Loaded && clip.loadState != AudioDataLoadState.Failed)
                        yield return null;// clip.LoadAudioData();

                    if (clip.loadState == AudioDataLoadState.Failed)
                        Debug.Log("  ! ERROR");

                    clip.name = file.Name;
                    proto.m_clip = clip;
                }
            }
        }

        // sound
        folder = new DirectoryInfo("Audio/Sounds");
        if (folder.Exists)
        {
            foreach (var proto in m_soundProtos)
            {
                var fileList = folder.GetFiles(proto.m_name + "*.ogg");
                AddArray(ref fileList, folder.GetFiles(proto.m_name + "*.wav"));
                bool first = true;
                for (int i = 0; i < fileList.Length; i++)
                {
                    var file = fileList[i];

                    string clipName = Path.GetFileNameWithoutExtension(file.Name);
                    if (clipName == proto.m_name || clipName.StartsWith(proto.m_name + "_"))
                    {
                        Debug.Log("  " + file.Name);

                        var www = new WWW("file://" + file.FullName);
                        var clip = www.GetAudioClip();

                        while (clip.loadState != AudioDataLoadState.Loaded && clip.loadState != AudioDataLoadState.Failed)
                            yield return null;// clip.LoadAudioData();

                        if (clip.loadState == AudioDataLoadState.Failed)
                            Debug.Log("  ! ERROR");

                        clip.name = file.Name;

                        if (first)
                        {
                            proto.m_clip = null;
                            first = false;
                        }

                        AddArrayElement<AudioClip>(ref proto.m_clip, clip);
                    }
                }
            }
        }

        Debug.Log("Loading audio end");

        if (playingMusic != "")
            PlayMusic(playingMusic, true);

        if (playingAmbient != "")
            PlayAmbient(playingAmbient);

        App.instance.m_busy--;
    }

    bool IsAudioFileType(string extension)
    {
        Debug.Log(extension);
        return (extension.ToUpper() == ".OGG" || extension.ToUpper() == ".WAV");
    }

    void SplitSoundFileName(string fileName, out string name, out int subidx, out int instancesCountMax)
    {
        subidx = 0;
        instancesCountMax = 0;
        name = fileName;

        string[] lst = name.Split(new char[] { ' ' });
        Debug.Assert(lst.Length > 0);
        if (lst.Length > 1)
        {
            int.TryParse(lst[1], out instancesCountMax);
            name = lst[0];
        }

        lst = name.Split(new char[] { '_' });
        if (lst.Length > 1)
        {
            int.TryParse(lst[1], out subidx);
            name = lst[0];
        }
    }

    void AddArray<T>(ref T[] a, T[] b)
    {
        var z = new T[a.Length + b.Length];
        a.CopyTo(z, 0);
        b.CopyTo(z, a.Length);
        a = z;
    }

    public static void AddArrayElement<T>(ref T[] target, T item)
    {
        int count = (target != null) ? target.Length : 0;
        T[] result = new T[count + 1];
        if (target != null)
            target.CopyTo(result, 0);
        result[count] = item;
        target = result;
    }

/*
        void OnGUI()
        {
            for(int i=0; i<m_voices.Length; i++)
            {
                SoundVoice v = m_voices[i];
                if(v.m_playing)
                    GUILayout.Label("v" + i + " sid=" + v.m_soundID + " playing=" + v.m_source.isPlaying);// + " activeenabled=" + v.m_source.isActiveAndEnabled);
            }
        }
    */
}
