

package com.shc.silenceengine.audio;

import com.shc.silenceengine.audio.openal.ALSource;
import com.shc.silenceengine.core.SilenceEngine;
import com.shc.silenceengine.utils.ReusableStack;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import static com.shc.silenceengine.audio.AudioDevice.Constants.*;

/**
 * <p>An AudioScene allows you to play positional sounds, in 3D. The scene updates itself whenever the game updates,
 * and also updates the position, direction and velocity of the updated sources. It acts like a master of all sounds.
 * </p>
 *
 * @author Sri Harsha Chilakapati
 */
public final class AudioScene
{
    private ReusableStack<ALSource>      sourcesPool;
    private ReusableStack<PlayingSource> playingSourcesPool;

    private Map<PlayingSource, AudioSource> playingSources;

    private AudioSource defaultAudioSource;

    /**
     * Prevent instantiation by users. Should only be used via {@code SilenceEngine.audio.scene}
     */
    AudioScene()
    {
        sourcesPool = new ReusableStack<>(ALSource::new);
        playingSourcesPool = new ReusableStack<>(PlayingSource::new);

        playingSources = new HashMap<>();
        defaultAudioSource = new AudioSource();

        SilenceEngine.eventManager.addDisposeHandler(this::cleanUp);
        SilenceEngine.eventManager.addUpdateHandler(this::updateSources);
    }

    /**
     * Stops all the sources and makes sure that no sound is playing from the game. This method does not stop the static
     * sounds of course because static sounds do not have a source attached.
     */
    public void stopAllSources()
    {
        for (PlayingSource source : playingSources.keySet())
        {
            source.alSource.stop();
            sourcesPool.push(source.alSource);
        }

        playingSources.clear();
    }

    private void cleanUp()
    {
        // Cleanup all the sources in the object pool
        for (ALSource source : sourcesPool.getAsList())
            source.dispose();
    }

    private void updateSources(float deltaTime)
    {
        Iterator<PlayingSource> iterator = playingSources.keySet().iterator();

        while (iterator.hasNext())
        {
            PlayingSource playingSource = iterator.next();
            ALSource source = playingSource.alSource;
            AudioSource audioSource = playingSources.get(playingSource);

            ALSource.State state = source.getState();

            if (state != ALSource.State.PLAYING &&
                state != ALSource.State.LOOPING)
            {
                source.attachBuffer(null);

                iterator.remove();
                sourcesPool.push(source);
                playingSourcesPool.push(playingSource);

                continue;
            }

            if (audioSource.updated)
            {
                source.pause();

                source.setParameter(AL_POSITION, audioSource.position);
                source.setParameter(AL_VELOCITY, audioSource.velocity);
                source.setParameter(AL_DIRECTION, audioSource.direction);

                audioSource.updated = false;
                source.play();
            }
        }
    }

    /**
     * Plays a sound as a static audio, that is, it has no position and special effects.
     *
     * @param sound The sound to be played.
     */
    public void playStatic(Sound sound)
    {
        play(sound, defaultAudioSource, false);
    }

    /**
     * Plays a static sound, optionally allowing you to loop the sound.
     *
     * @param sound The sound to be played.
     * @param loop  Whether to loop the sound.
     */
    public void playStatic(Sound sound, boolean loop)
    {
        play(sound, defaultAudioSource, loop);
    }

    /**
     * Plays a sound through a specified AudioSource. The AudioSource instance specifies the spatial properties of the
     * sound to be played.
     *
     * @param sound  The Sound object to be played.
     * @param source The AudioSource object which describes the spatial properties.
     */
    public void play(Sound sound, AudioSource source)
    {
        play(sound, source, false);
    }

    /**
     * Plays a sound through a specified AudioSource, optionally allowing you to loop the sound. The AudioSource
     * instance specifies the spatial properties of the sound to be played.
     *
     * @param sound  The Sound object to be played.
     * @param source The AudioSource object which describes the spatial properties.
     * @param loop   Whether to play the sound in loop.
     */
    public void play(Sound sound, AudioSource source, boolean loop)
    {
        ALSource alSource = sourcesPool.pop();

        source.update();

        alSource.attachBuffer(sound.buffer);
        alSource.setParameter(AL_POSITION, source.position);
        alSource.setParameter(AL_VELOCITY, source.velocity);
        alSource.setParameter(AL_DIRECTION, source.direction);
        alSource.setParameter(AL_LOOPING, loop);

        source.updated = false;
        alSource.play();

        PlayingSource playingSource = playingSourcesPool.pop();
        playingSource.sound = sound;
        playingSource.alSource = alSource;

        playingSources.put(playingSource, source);
    }

    /**
     * Stops a sound which is playing from all the sources. Other sounds for the sources will not be effected.
     *
     * @param sound The sound to stop playing.
     */
    public void stopFromAllSources(Sound sound)
    {
        for (PlayingSource source : playingSources.keySet())
        {
            if (source.sound.buffer.getID() == sound.buffer.getID())
                source.alSource.stop();
        }
    }

    /**
     * Stops all the sounds that are playing from a specific source.
     *
     * @param source The source from which the sounds should be stopped.
     */
    public void stopAllFromSource(AudioSource source)
    {
        for (PlayingSource playingSource : playingSources.keySet())
        {
            if (playingSources.get(playingSource) == source)
                playingSource.alSource.stop();
        }
    }

    /**
     * Stops the sound which is being played statically without any spatial properties.
     *
     * @param sound The sound to be stopped.
     */
    public void stopStatic(Sound sound)
    {
        stop(sound, defaultAudioSource);
    }

    /**
     * Stops a sound from playing from a specific source.
     *
     * @param sound  The sound to be stopped.
     * @param source The source which is playing that sound.
     */
    public void stop(Sound sound, AudioSource source)
    {
        for (PlayingSource playingSource : playingSources.keySet())
        {
            if (playingSource.sound.buffer.getID() == sound.buffer.getID())
                if (playingSources.get(playingSource) == source)
                    playingSource.alSource.stop();
        }
    }

    private static class PlayingSource
    {
        private ALSource alSource;
        private Sound    sound;
    }
}
