//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.openal;

import java.util.Map;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

import java.nio.ByteBuffer;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;

import org.lwjgl.openal.AL10;
import org.lwjgl.util.WaveData;

import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;

import com.samskivert.util.BasicRunQueue;
import com.samskivert.util.RandomUtil;
import com.samskivert.util.ResultListener;
import com.samskivert.util.RunQueue;

import com.threerings.media.FrameManager;
import com.threerings.media.sound.JavaSoundPlayer;
import com.threerings.media.sound.SoundLoader;
import com.threerings.media.sound.SoundPlayer;
import com.threerings.media.timer.MediaTimer;

import static com.threerings.media.Log.log;

/**
 * Implements the abstract pieces of {@link SoundPlayer} via OpenAL.
 */
public class OpenALSoundPlayer extends SoundPlayer
    implements ClipProvider
{

    public OpenALSoundPlayer (SoundLoader loader)
    {
        _loader = loader;
        try {
            _alSoundManager = createSoundManager();
            _group = _alSoundManager.createGroup(this, SOURCE_COUNT);
        } catch (Throwable t) {
            log.warning("Unable to initialize OpenAL", "cause", t);
        }
        _ticker.start();
    }

    public Clip loadClip (String path)
        throws IOException
    {
        int bundleEnd = path.lastIndexOf(":");
        InputStream sound = _loader.getSound(path.substring(0, bundleEnd),
            path.substring(bundleEnd + 1));
        if (path.endsWith(".ogg")) {
            try {
                AudioInputStream instream = JavaSoundPlayer.setupAudioStream(sound);
                ByteArrayOutputStream outstream = new ByteArrayOutputStream();

                byte[] buf = new byte[16 * 1024];
                int read;
                do {
                    read = instream.read(buf, 0, buf.length);
                    if (read >= 0) {
                        outstream.write(buf, 0, read);
                    }
                } while (read >= 0);

                byte[] audio = outstream.toByteArray();
                AudioFormat format = instream.getFormat();
                long length = audio.length / format.getFrameSize();

                instream = new AudioInputStream(new ByteArrayInputStream(audio), format, length);

                outstream = new ByteArrayOutputStream();

                AudioSystem.write(instream, AudioFileFormat.Type.WAVE, outstream);

                sound = new ByteArrayInputStream(outstream.toByteArray());

            } catch (Exception e) {
                log.warning("Error decompressing audio clip", "path", path, e);
                return new Clip();
            }
        }
        return new Clip(WaveData.create(sound));
    }

    /**
     * Returns the loader used by this player.
     */
    public SoundLoader getSoundLoader ()
    {
        return _loader;
    }

    @Override
    public RunQueue getSoundQueue ()
    {
        return _ticker;
    }

    @Override
    public void setClipVolume (final float vol)
    {
        super.setClipVolume(vol);
        getSoundQueue().postRunnable(new Runnable() {
            public void run () {
                _alSoundManager.setBaseGain(vol);
            }});
    }

    @Override
    public void lock (String pkgPath, String... keys)
    {
        for (String key : keys) {
            for (final String path : getPaths(pkgPath, key)) {
                getSoundQueue().postRunnable(new Runnable() {
                    public void run () {
                        if (_locked.containsKey(path)) {
                            return;
                        }
                        _alSoundManager.loadClip(OpenALSoundPlayer.this, path,
                            new ClipBuffer.Observer() {
                                public void clipFailed (ClipBuffer buffer) {
                                    log.warning("Unable to load sound", "path", path);
                                }

                                public void clipLoaded (ClipBuffer buffer) {
                                    _locked.put(path, buffer);
                                }
                            });
                    }
                });
            }
        }
    }

    @Override
    public void unlock (final String pkgPath, String... keys)
    {
        for (final String key : keys) {
            getSoundQueue().postRunnable(new Runnable() {
                public void run () {
                    for (String path : getPaths(pkgPath, key)) {
                        _locked.remove(path);
                    }
                }
            });
        }
    }

    /**
     * Streams ogg files from the given bundle and path.
     */
    public void stream (final String bundle, final String path, final boolean loop,
        final ResultListener<Stream> listener)
        throws IOException
    {
        if (!path.endsWith(".ogg")) {
            log.warning("Unknown file type for streaming", "bundle", bundle, "path", path);
            return;
        }
        InputStream rsrc = _loader.getSound(bundle, path);
        final StreamDecoder dec = new OggStreamDecoder();
        dec.init(rsrc);
        getSoundQueue().postRunnable(new Runnable() {
            public void run () {
                Stream s = new Stream(_alSoundManager) {
                    @Override
                    protected void update (float time) {
                        super.update(time);
                        if (_state != AL10.AL_PLAYING) {
                            return;
                        }
                        super.setGain(_clipVol * _streamGain);
                    }

                    @Override
                    public void setGain (float gain) {
                        _streamGain = gain;
                        super.setGain(_clipVol * _streamGain);
                    }

                    @Override
                    protected int getFormat () {
                        return dec.getFormat();
                    }

                    @Override
                    protected int getFrequency () {
                        return dec.getFrequency();
                    }

                    @Override
                    protected int populateBuffer (ByteBuffer buf) throws IOException {
                        int read = dec.read(buf);
                        if (buf.hasRemaining() && loop) {
                            dec.init(_loader.getSound(bundle, path));
                            read = Math.max(0, read);
                            read += dec.read(buf);
                        }
                        return read;
                    }

                    protected float _streamGain = 1F;
                };
                s.setGain(_clipVol);
                listener.requestCompleted(s);
            }});
    }

    @Override
    protected Frob loop (String pkgPath, String key, float pan)
    {
        final float volume = getClipVolume();
        final SoundGrabber loader = new SoundGrabber(pkgPath, key) {
            @Override
            protected void soundLoaded () {
                sound.setGain(volume);
                sound.loop(true);
            }};
        getSoundQueue().postRunnable(loader);
        return new Frob() {
            public float getPan () {
                return 0;
            }

            public float getVolume () {
                return 0;
            }

            public void setPan (float pan) {}

            public void setVolume (float vol) {}

            public void stop () {
                getSoundQueue().postRunnable(new Runnable(){
                    public void run () {
                        if (loader.sound != null) {
                            loader.sound.stop();
                        }
                    }});
            }};
    }

    @Override
    public void play (String pkgPath, String key, float pan)
    {
        play(pkgPath, key, pan, getClipVolume());
    }

    public void play (String pkgPath, String key, float pan, final float gain)
    {
        play(null, pkgPath, key, gain, null);
    }

    public boolean play (SoundType type, String pkgPath, String key, final float gain,
        final float[] pos)
    {
        if (type != null && !shouldPlay(type)) {
            return false;
        }

        getSoundQueue().postRunnable(new SoundGrabber(pkgPath, key) {
            @Override
            protected void soundLoaded () {
                sound.setGain(gain);
                if (pos != null) {
                    sound.setPosition(pos[0], pos[1], pos[2]);
                }
                sound.play(true);
            }
        });
        return true;
    }

    @Override
    public void shutdown ()
    {
        getSoundQueue().postRunnable(new Runnable() {
            public void run () {
                _group.dispose();
                _locked.clear();
                for (Stream stream : _alSoundManager.getStreams()) {
                    stream.dispose();
                }
            }
        });
    }

    /**
     * Returns bundle:path for all sounds under key in pkgPath.
     */
    protected String[] getPaths (String pkgPath, String key)
    {

        String bundle = _loader.getBundle(pkgPath);
        Preconditions.checkNotNull(bundle,
            "Unable to find the bundle name for a package [package=%s, key=%s]", pkgPath, key);
        String[] names = _loader.getPaths(pkgPath, key);
        Preconditions.checkNotNull(names, "No such sound [package=%s, key=%s]", pkgPath, key);
        String[] paths = new String[names.length];
        for (int ii = 0; ii < paths.length; ii++) {
            paths[ii] = bundle + ":" + names[ii];
        }
        return paths;
    }

    /**
     * Creates our SoundManager.
     */
    protected SoundManager createSoundManager ()
    {
        return new MediaALSoundManager();
    }

    /**
     * Extends sound manager to allow sounds to be pulled out of the locked map.
     */
    protected class MediaALSoundManager extends SoundManager {
        protected MediaALSoundManager () {
            super(getSoundQueue());
        }

        @Override
        protected ClipBuffer getClip (ClipProvider provider, String path) {
            if (_locked.containsKey(path)) {
                return _locked.get(path);
            }
            return super.getClip(provider, path, null);
        }
    }

    /**
     * Updates the sound manager's streams every STREAM_UPDATE_INTERVAL and processes sound
     * runnables added to its queue.
     */
    protected class TickingQueue extends BasicRunQueue
    {
        public TickingQueue ()
        {
            super("SoundPlayerQueue");
        }

        @Override
        protected void iterate ()
        {
            long elapsed = _timer.getElapsedMillis() - _lastTick;
            Runnable r;
            if (elapsed >= STREAM_UPDATE_INTERVAL) {
                r = _queue.getNonBlocking();
            } else {
                r = _queue.get(STREAM_UPDATE_INTERVAL - elapsed);
            }
            long newTime;
            if (_alSoundManager == null) {
                // We weren't able to initialize the sound system, and we logged it earlier, so
                // just empty the queue and update the tick time without running the code that
                // needs the sound manager.
                newTime = _timer.getElapsedMillis();
            } else {
                if (r != null) {
                    try {
                        r.run();

                    } catch (Throwable t) {
                        log.warning("Runnable posted to SoundPlayerQueue barfed.", t);
                    }
                }
                newTime = _timer.getElapsedMillis();
                try {
                    _alSoundManager.updateStreams((newTime - _lastTick) / 1000F);
                } catch (Throwable t) {
                    log.warning("Updating OpenAL streams barfed.", t);
                }
            }
            _lastTick = newTime;
        }

        protected MediaTimer _timer = FrameManager.createTimer();

        protected long _lastTick;
    }

    /**
     * Loads a sound in its run method and calls subclasses with soundLoaded to let them know it's
     * ready.
     */
    protected abstract class SoundGrabber
        implements Runnable
    {
        public String path;

        public Sound sound;

        public SoundGrabber (String pkgPath, String key)
        {
            String[] paths = getPaths(pkgPath, key);
            path = paths[RandomUtil.getInt(paths.length)];
        }

        public void run ()
        {
            sound = _group.getSound(path);
            soundLoaded();
        }

        protected abstract void soundLoaded ();
    }

    /** Number of milliseconds to wait between stream updates. */
    protected static final int STREAM_UPDATE_INTERVAL = 100;

    protected TickingQueue _ticker = new TickingQueue();

    protected Map<String, ClipBuffer> _locked = Maps.newHashMap();

    protected SoundLoader _loader;
    protected SoundGroup _group;
    protected SoundManager _alSoundManager;

    /** Number of sounds that can be played simultaneously. */
    protected final int SOURCE_COUNT = 10;
}
