/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.badlogic.gdx.backends.lwjgl.audio;

import java.nio.FloatBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;

import com.badlogic.gdx.Audio;
import com.badlogic.gdx.audio.AudioDevice;
import com.badlogic.gdx.audio.AudioRecorder;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.IntArray;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.LongMap;
import com.badlogic.gdx.utils.ObjectMap;

import static org.lwjgl.openal.AL10.*;

/** @author Nathan Sweet */
public class OpenALAudio implements Audio {
    private final int deviceBufferSize;
    private final int deviceBufferCount;
    private IntArray idleSources, allSources;
    private LongMap<Integer> soundIdToSource;
    private IntMap<Long> sourceToSoundId;
    private long nextSoundId = 0;
    private ObjectMap<String, Class<? extends OpenALSound>> extensionToSoundClass = new ObjectMap();
    private ObjectMap<String, Class<? extends OpenALMusic>> extensionToMusicClass = new ObjectMap();
    private OpenALSound[] recentSounds;
    private int mostRecetSound = -1;

    Array<OpenALMusic> music = new Array(false, 1, OpenALMusic.class);
    boolean noDevice = false;

    public OpenALAudio () {
        this(16, 9, 512);
    }

    public OpenALAudio (int simultaneousSources, int deviceBufferCount, int deviceBufferSize) {
        this.deviceBufferSize = deviceBufferSize;
        this.deviceBufferCount = deviceBufferCount;

        registerSound("ogg", Ogg.Sound.class);
        registerMusic("ogg", Ogg.Music.class);
        registerSound("wav", Wav.Sound.class);
        registerMusic("wav", Wav.Music.class);
        registerSound("mp3", Mp3.Sound.class);
        registerMusic("mp3", Mp3.Music.class);

        try {
            AL.create();
        } catch (LWJGLException ex) {
            noDevice = true;
            ex.printStackTrace();
            return;
        }

        allSources = new IntArray(false, simultaneousSources);
        for (int i = 0; i < simultaneousSources; i++) {
            int sourceID = alGenSources();
            if (alGetError() != AL_NO_ERROR) break;
            allSources.add(sourceID);
        }
        idleSources = new IntArray(allSources);
        soundIdToSource = new LongMap<Integer>();
        sourceToSoundId = new IntMap<Long>();

        FloatBuffer orientation = (FloatBuffer)BufferUtils.createFloatBuffer(6)
                                                          .put(new float[] {0.0f, 0.0f, 1.0f, 0.0f, -1.0f, 0.0f}).flip();
        alListener(AL_ORIENTATION, orientation);
        FloatBuffer velocity = (FloatBuffer)BufferUtils.createFloatBuffer(3).put(new float[] {0.0f, 0.0f, 0.0f}).flip();
        alListener(AL_VELOCITY, velocity);
        FloatBuffer position = (FloatBuffer)BufferUtils.createFloatBuffer(3).put(new float[] {0.0f, 0.0f, 0.0f}).flip();
        alListener(AL_POSITION, position);

        recentSounds = new OpenALSound[simultaneousSources];
    }

    public void registerSound (String extension, Class<? extends OpenALSound> soundClass) {
        if (extension == null) throw new IllegalArgumentException("extension cannot be null.");
        if (soundClass == null) throw new IllegalArgumentException("soundClass cannot be null.");
        extensionToSoundClass.put(extension, soundClass);
    }

    public void registerMusic (String extension, Class<? extends OpenALMusic> musicClass) {
        if (extension == null) throw new IllegalArgumentException("extension cannot be null.");
        if (musicClass == null) throw new IllegalArgumentException("musicClass cannot be null.");
        extensionToMusicClass.put(extension, musicClass);
    }

    public OpenALSound newSound (FileHandle file) {
        if (file == null) throw new IllegalArgumentException("file cannot be null.");
        Class<? extends OpenALSound> soundClass = extensionToSoundClass.get(file.extension().toLowerCase());
        if (soundClass == null) throw new GdxRuntimeException("Unknown file extension for sound: " + file);
        try {
            return soundClass.getConstructor(new Class[] {OpenALAudio.class, FileHandle.class}).newInstance(this, file);
        } catch (Exception ex) {
            throw new GdxRuntimeException("Error creating sound " + soundClass.getName() + " for file: " + file, ex);
        }
    }

    public OpenALMusic newMusic (FileHandle file) {
        if (file == null) throw new IllegalArgumentException("file cannot be null.");
        Class<? extends OpenALMusic> musicClass = extensionToMusicClass.get(file.extension().toLowerCase());
        if (musicClass == null) throw new GdxRuntimeException("Unknown file extension for music: " + file);
        try {
            return musicClass.getConstructor(new Class[] {OpenALAudio.class, FileHandle.class}).newInstance(this, file);
        } catch (Exception ex) {
            throw new GdxRuntimeException("Error creating music " + musicClass.getName() + " for file: " + file, ex);
        }
    }

    int obtainSource (boolean isMusic) {
        if (noDevice) return 0;
        for (int i = 0, n = idleSources.size; i < n; i++) {
            int sourceId = idleSources.get(i);
            int state = alGetSourcei(sourceId, AL_SOURCE_STATE);
            if (state != AL_PLAYING && state != AL_PAUSED) {
                if (isMusic) {
                    idleSources.removeIndex(i);
                } else {
                    if (sourceToSoundId.containsKey(sourceId)) {
                        long soundId = sourceToSoundId.get(sourceId);
                        sourceToSoundId.remove(sourceId);
                        soundIdToSource.remove(soundId);
                    }

                    long soundId = nextSoundId++;
                    sourceToSoundId.put(sourceId, soundId);
                    soundIdToSource.put(soundId, sourceId);
                }
                alSourceStop(sourceId);
                alSourcei(sourceId, AL_BUFFER, 0);
                AL10.alSourcef(sourceId, AL10.AL_GAIN, 1);
                AL10.alSourcef(sourceId, AL10.AL_PITCH, 1);
                AL10.alSource3f(sourceId, AL10.AL_POSITION, 0, 0, 1f);
                return sourceId;
            }
        }
        return -1;
    }

    void freeSource (int sourceID) {
        if (noDevice) return;
        alSourceStop(sourceID);
        alSourcei(sourceID, AL_BUFFER, 0);
        if (sourceToSoundId.containsKey(sourceID)) {
            long soundId = sourceToSoundId.remove(sourceID);
            soundIdToSource.remove(soundId);
        }
        idleSources.add(sourceID);
    }

    void freeBuffer (int bufferID) {
        if (noDevice) return;
        for (int i = 0, n = idleSources.size; i < n; i++) {
            int sourceID = idleSources.get(i);
            if (alGetSourcei(sourceID, AL_BUFFER) == bufferID) {
                if (sourceToSoundId.containsKey(sourceID)) {
                    long soundId = sourceToSoundId.remove(sourceID);
                    soundIdToSource.remove(soundId);
                }
                alSourceStop(sourceID);
                alSourcei(sourceID, AL_BUFFER, 0);
            }
        }
    }

    void stopSourcesWithBuffer (int bufferID) {
        if (noDevice) return;
        for (int i = 0, n = idleSources.size; i < n; i++) {
            int sourceID = idleSources.get(i);
            if (alGetSourcei(sourceID, AL_BUFFER) == bufferID) {
                if (sourceToSoundId.containsKey(sourceID)) {
                    long soundId = sourceToSoundId.remove(sourceID);
                    soundIdToSource.remove(soundId);
                }
                alSourceStop(sourceID);
            }
        }
    }

    void pauseSourcesWithBuffer (int bufferID) {
        if (noDevice) return;
        for (int i = 0, n = idleSources.size; i < n; i++) {
            int sourceID = idleSources.get(i);
            if (alGetSourcei(sourceID, AL_BUFFER) == bufferID)
                alSourcePause(sourceID);
        }
    }

    void resumeSourcesWithBuffer (int bufferID) {
        if (noDevice) return;
        for (int i = 0, n = idleSources.size; i < n; i++) {
            int sourceID = idleSources.get(i);
            if (alGetSourcei(sourceID, AL_BUFFER) == bufferID) {
                if (alGetSourcei(sourceID, AL_SOURCE_STATE) == AL_PAUSED)
                    alSourcePlay(sourceID);
            }
        }
    }

    public void update () {
        if (noDevice) return;
        for (int i = 0; i < music.size; i++)
            music.items[i].update();
    }

    public long getSoundId (int sourceId) {
        if (!sourceToSoundId.containsKey(sourceId)) return -1;
        return sourceToSoundId.get(sourceId);
    }

    public void stopSound (long soundId) {
        if (!soundIdToSource.containsKey(soundId)) return;
        int sourceId = soundIdToSource.get(soundId);
        alSourceStop(sourceId);
    }

    public void pauseSound (long soundId) {
        if (!soundIdToSource.containsKey(soundId)) return;
        int sourceId = soundIdToSource.get(soundId);
        alSourcePause(sourceId);
    }

    public void resumeSound (long soundId) {
        if (!soundIdToSource.containsKey(soundId)) return;
        int sourceId = soundIdToSource.get(soundId);
        if (alGetSourcei(sourceId, AL_SOURCE_STATE) == AL_PAUSED)
            alSourcePlay(sourceId);
    }

    public void setSoundGain (long soundId, float volume) {
        if (!soundIdToSource.containsKey(soundId)) return;
        int sourceId = soundIdToSource.get(soundId);
        AL10.alSourcef(sourceId, AL10.AL_GAIN, volume);
    }

    public void setSoundLooping (long soundId, boolean looping) {
        if (!soundIdToSource.containsKey(soundId)) return;
        int sourceId = soundIdToSource.get(soundId);
        alSourcei(sourceId, AL10.AL_LOOPING, looping ? AL10.AL_TRUE : AL10.AL_FALSE);
    }

    public void setSoundPitch (long soundId, float pitch) {
        if (!soundIdToSource.containsKey(soundId)) return;
        int sourceId = soundIdToSource.get(soundId);
        AL10.alSourcef(sourceId, AL10.AL_PITCH, pitch);
    }

    public void setSoundPan (long soundId, float pan, float volume) {
        if (!soundIdToSource.containsKey(soundId)) return;
        int sourceId = soundIdToSource.get(soundId);

        AL10.alSource3f(sourceId, AL10.AL_POSITION, MathUtils.cos((pan - 1) * MathUtils.PI / 2), 0,
                MathUtils.sin((pan + 1) * MathUtils.PI / 2));
        AL10.alSourcef(sourceId, AL10.AL_GAIN, volume);
    }

    public void dispose () {
        if (noDevice) return;
        for (int i = 0, n = allSources.size; i < n; i++) {
            int sourceID = allSources.get(i);
            int state = alGetSourcei(sourceID, AL_SOURCE_STATE);
            if (state != AL_STOPPED) alSourceStop(sourceID);
            alDeleteSources(sourceID);
        }

        sourceToSoundId.clear();
        soundIdToSource.clear();

        AL.destroy();
        while (AL.isCreated()) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
            }
        }
    }

    public AudioDevice newAudioDevice (int sampleRate, final boolean isMono) {
        if (noDevice) return new AudioDevice() {
            @Override
            public void writeSamples (float[] samples, int offset, int numSamples) {
            }

            @Override
            public void writeSamples (short[] samples, int offset, int numSamples) {
            }

            @Override
            public void setVolume (float volume) {
            }

            @Override
            public boolean isMono () {
                return isMono;
            }

            @Override
            public int getLatency () {
                return 0;
            }

            @Override
            public void dispose () {
            }
        };
        return new OpenALAudioDevice(this, sampleRate, isMono, deviceBufferSize, deviceBufferCount);
    }

    public AudioRecorder newAudioRecorder (int samplingRate, boolean isMono) {
        if (noDevice) return new AudioRecorder() {
            @Override
            public void read (short[] samples, int offset, int numSamples) {
            }

            @Override
            public void dispose () {
            }
        };
        return new JavaSoundAudioRecorder(samplingRate, isMono);
    }

    /** Retains a list of the most recently played sounds and stops the sound played least recently if necessary for a new sound to
     * play */
    protected void retain (OpenALSound sound, boolean stop) {
        // Move the pointer ahead and wrap
        mostRecetSound++;
        mostRecetSound %= recentSounds.length;

        if (stop) {
            // Stop the least recent sound (the one we are about to bump off the buffer)
            if (recentSounds[mostRecetSound] != null) recentSounds[mostRecetSound].stop();
        }

        recentSounds[mostRecetSound] = sound;
    }

    /** Removes the disposed sound from the least recently played list */
    public void forget (OpenALSound sound) {
        for (int i = 0; i < recentSounds.length; i++) {
            if (recentSounds[i] == sound) recentSounds[i] = null;
        }
    }
}
