/*
Author:		Jan Lygnos
Date:		12.8.2019
Last Edit:  13.8.2019
*/

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

/// <summary>
/// Enables playing audio from objects with lots of customizable properties.
/// </summary>
public class AudioManager : SingletonDontDestroyOnLoad<AudioManager> {

    public List<PlayingAudio> PlayingAudioSources = new List<PlayingAudio>();
    public int VolumeChangePerSecond = 15;
    public float FadeDuration = 0.1f;
    public float CutsceneModeVolume = 0.2f;

    private const float DEFAULT_VOLUME = 1f;

    private AudioSource[] atmosphericSources = new AudioSource[2];
    private IEnumerator[] fader = new IEnumerator[2];
    private int activeSource;

    public void PlayGameSound() => PlayAtmosphere("Game");

    /// <summary>
    /// Requests to play audio and returns the created AudioSource component.
    /// </summary>
    /// <param name="sender">the component that is sending a request</param>
    /// <param name="name">the audios name</param>
    /// <param name="loop">should the audio loop?</param>
    /// <param name="volume">the audios volume</param>
    /// <param name="spatial">is the source spatial?</param>
    /// <param name="onEndPlaying">callback on when playing ended</param>
    /// <param name="radius">the AudioSource's radius</param>
    /// <returns>The audio source currently playing.</returns>
    public AudioSource PlayGetAudioSource(Component sender, string name, bool loop = false, float volume = DEFAULT_VOLUME, bool spatial = true, Action onEndPlaying = null, float radius = 500f) {
        Audio audio = AudioDatabase.Instance.AccessAudio(name);

        if (audio == null) {
            Debug.LogError($"There is no Audio called \"{name}\" in the Audio database!");
            return null;
        }

        if (!audio.AudioClip) {
            Debug.LogError($"There is no AudioClip assigned for the Audio called \"{name}\"!");
            return null;
        }

        AudioSource source = sender.gameObject.AddComponent<AudioSource>();
        if (spatial) source.spatialBlend = 1.0f;
        source.clip = audio.AudioClip;
        source.maxDistance = radius;
        source.volume = volume == DEFAULT_VOLUME ? audio.Volume : volume;
        source.loop = loop;
        source.Play();

        StartCoroutine(AudioClipStarted(source, false, onEndPlaying));

        return source;
    }

    /// <summary>
    /// [MORE PERFORMANT] Requests to play audio and returns the created AudioSource component by ID.
    /// </summary>
    /// <param name="sender">the component that is sending a request</param>
    /// <param name="id">the audios reference id</param>
    /// <param name="loop">should the audio loop?</param>
    /// <param name="volume">the audios volume</param>
    /// <param name="spatial">is the source spatial?</param>
    /// <param name="onEndPlaying">callback on when playing ended</param>
    /// <param name="radius">the AudioSource's radius</param>
    /// <returns>The audio source currently playing.</returns>
    public AudioSource PlayGetAudioSource(Component sender, int id, bool loop = false, float volume = DEFAULT_VOLUME, bool spatial = true, Action onEndPlaying = null, float radius = 500f) {
        Audio audio = AudioDatabase.Instance.AccessAudio(id);

        if (audio == null) {
            Debug.LogError($"There is no Audio called \"{name}\" in the Audio database!");
            return null;
        }

        if (!audio.AudioClip) {
            Debug.LogError($"There is no AudioClip assigned for the Audio called \"{name}\"!");
            return null;
        }

        AudioSource source = sender.gameObject.AddComponent<AudioSource>();
        if (spatial) source.spatialBlend = 1.0f;
        source.clip = audio.AudioClip;
        source.maxDistance = radius;
        source.volume = volume == DEFAULT_VOLUME ? audio.Volume : volume;
        source.loop = loop;
        source.Play();

        StartCoroutine(AudioClipStarted(source, false, onEndPlaying));

        return source;
    }

    /// <summary>
    /// Requests to play audio.
    /// </summary>
    /// <param name="sender">the component that is sending a request</param>
    /// <param name="name">the audios name</param>
    /// <param name="loop">should the audio loop?</param>
    /// <param name="volume">the audios volume</param>
    /// <param name="spatial">is the source spatial?</param>
    /// <param name="onEndPlaying">callback on when playing ended</param>
    public void Play(Component sender, string name, bool loop = false, float volume = DEFAULT_VOLUME, bool spatial = true, Action onEndPlaying = null) {
        PlayGetAudioSource(sender, name, loop, volume, spatial, onEndPlaying);
    }

    /// <summary>
    /// [MORE PERFORMANT] Requests to play audio.
    /// </summary>
    /// <param name="sender">the component that is sending a request</param>
    /// <param name="id">the audios reference id</param>
    /// <param name="loop">should the audio loop?</param>
    /// <param name="volume">the audios volume</param>
    /// <param name="spatial">is the source spatial?</param>
    /// <param name="onEndPlaying">callback on when playing ended</param>
    public void Play(Component sender, int id, bool loop = false, float volume = DEFAULT_VOLUME, bool spatial = true, Action onEndPlaying = null) {
        PlayGetAudioSource(sender, id, loop, volume, spatial, onEndPlaying);
    }

    /// <summary>
    /// Requests to play athmospheric audio.
    /// </summary>
    /// <param name="name">the audios name</param>
    /// <param name="volume">the audios volume</param>
    public void PlayAtmosphere(string name, float volume = DEFAULT_VOLUME) {
        Audio audio = AudioDatabase.Instance.AccessAudio(name);
        PlayingAudio current = null;
        PlayingAudio next = null;

        if (audio == null) {
            Debug.LogError($"There is no Audio called \"{name}\" in the Audio database!");
            return;
        }

        if (!audio.AudioClip) {
            Debug.LogError($"There is no AudioClip assigned for the Audio called \"{name}\"!");
            return;
        }

        if (!(atmosphericSources[0] && atmosphericSources[1])) {
            atmosphericSources[0] = SetupSource();
            atmosphericSources[1] = SetupSource();
        }

        current = new PlayingAudio(atmosphericSources[activeSource], Time.realtimeSinceStartup, true);

        if (atmosphericSources[activeSource].volume > 0) {
            PlayingAudioSources.Add(current);


            fader[0] = FadeAudioSource(atmosphericSources[activeSource], FadeDuration, 0.0f, () => { fader[0] = null; PlayingAudioSources.Remove(next); });
            StartCoroutine(fader[0]);
        }

        int nextSource = (activeSource + 1) % atmosphericSources.Length;
        atmosphericSources[nextSource].clip = audio.AudioClip;
        atmosphericSources[nextSource].Play();

        next = new PlayingAudio(atmosphericSources[nextSource], Time.realtimeSinceStartup, true);
        PlayingAudioSources.Add(next);

        fader[1] = FadeAudioSource(atmosphericSources[nextSource], FadeDuration, volume, () => { fader[1] = null; PlayingAudioSources.Remove(current); });
        StartCoroutine(fader[1]);

        activeSource = nextSource;
    }

    /// <summary>
    /// Toggles down atmospheric sound volume.
    /// </summary>
    /// <param name="active">Is the cutscene mode active?</param>
    public void SetAtmosphereCutsceneMode(bool active) {
        for (int i = 0; i < atmosphericSources.Length; i++) {
            atmosphericSources[i].volume = active ? CutsceneModeVolume : 1f;// initialVolumes[i];
        }
    }

    private IEnumerator AudioClipStarted(AudioSource source, bool isAtmospheric = false, Action onEnd = null) {
        PlayingAudio current = new PlayingAudio(source, Time.realtimeSinceStartup, isAtmospheric);
        PlayingAudioSources.Add(current);
        yield return new WaitForSeconds(source.clip.length);
        if (source && !source.loop) AudioClipEnded(current, onEnd);
    }

    private void AudioClipEnded(PlayingAudio playing, Action onEnd) {
        Destroy(playing.Source);
        if (!PlayingAudioSources.Remove(playing)) Debug.LogWarning($"AudioManager couldn't find AudioSource playing {playing.Source.clip} on GameObject {playing.Source.gameObject.name}!");
        onEnd?.Invoke();
    }

    private IEnumerator FadeAudioSource(AudioSource player, float duration, float targetVolume, Action onFinished) {
        int Steps = (int)(VolumeChangePerSecond * duration);
        float StepTime = duration / Steps;
        float StepSize = (targetVolume - player.volume) / Steps;

        for (int i = 1; i < Steps; i++) {
            player.volume += StepSize;
            yield return new WaitForSeconds(StepTime);
        }
        player.volume = targetVolume;

        if (onFinished != null) {
            onFinished();
        }
    }

    private AudioSource SetupSource() {
        AudioSource src = gameObject.AddComponent<AudioSource>();
        src.playOnAwake = false;
        src.volume = 0.0f;
        src.loop = true;
        return src;
    }
}