//
// $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.io.IOException;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;

import org.lwjgl.openal.AL10;

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

/**
 * Represents a streaming source of sound data.
 */
public abstract class Stream
{
    /**
     * Creates a new stream.  Call {@link #dispose} when finished with the stream.
     *
     * @param soundmgr a reference to the sound manager that will update the stream
     */
    public Stream (SoundManager soundmgr)
    {
        _soundmgr = soundmgr;

        // create the source and buffers
        _source = new Source(soundmgr);
        for (int ii = 0; ii < _buffers.length; ii++) {
            _buffers[ii] = new Buffer(soundmgr);
        }

        // register with sound manager
        _soundmgr.addStream(this);
    }

    /**
     * Sets the base gain of the stream.
     */
    public void setGain (float gain)
    {
        _gain = gain;
        if (_fadeMode == FadeMode.NONE) {
            _source.setGain(_gain);
        }
    }

    /**
     * Returns a reference to the stream source.
     */
    public Source getSource ()
    {
        return _source;
    }

    /**
     * Determines whether this stream is currently playing.
     */
    public boolean isPlaying ()
    {
        return _state == AL10.AL_PLAYING;
    }

    /**
     * Starts playing this stream.
     */
    public void play ()
    {
        if (_state == AL10.AL_PLAYING) {
            log.warning("Tried to play stream already playing.");
            return;
        }
        if (_state == AL10.AL_INITIAL) {
            _qidx = _qlen = 0;
            queueBuffers(_buffers.length);
        }
        _source.play();
        _state = AL10.AL_PLAYING;
    }

    /**
     * Pauses this stream.
     */
    public void pause ()
    {
        if (_state != AL10.AL_PLAYING) {
            log.warning("Tried to pause stream that wasn't playing.");
            return;
        }
        _source.pause();
        _state = AL10.AL_PAUSED;
    }

    /**
     * Stops this stream.
     */
    public void stop ()
    {
        if (_state == AL10.AL_STOPPED) {
            log.warning("Tried to stop stream that was already stopped.");
            return;
        }
        _source.stop();
        _state = AL10.AL_STOPPED;
    }

    /**
     * Fades this stream in over the specified interval.  If the stream isn't playing, it will be
     * started.
     */
    public void fadeIn (float interval)
    {
        if (_state != AL10.AL_PLAYING) {
            play();
        }
        _source.setGain(0f);
        _fadeMode = FadeMode.IN;
        _fadeInterval = interval;
        _fadeElapsed = 0f;
    }

    /**
     * Fades this stream out over the specified interval.
     *
     * @param dispose if true, dispose of the stream when done fading out
     */
    public void fadeOut (float interval, boolean dispose)
    {
        _fadeMode = dispose ? FadeMode.OUT_DISPOSE : FadeMode.OUT;
        _fadeInterval = interval;
        _fadeElapsed = 0f;
    }

    /**
     * Releases the resources held by this stream and removes it from the manager.
     */
    public void dispose ()
    {
        // make sure the stream is stopped
        if (_state != AL10.AL_STOPPED) {
            stop();
        }

        // delete the source and buffers
        _source.delete();
        for (Buffer buffer : _buffers) {
            buffer.delete();
        }

        // remove from manager
        _soundmgr.removeStream(this);
    }

    /**
     * Updates the state of this stream, loading data into buffers and adjusting gain as necessary.
     * Called periodically by the {@link SoundManager}.
     *
     * @param time the amount of time elapsed since the last update
     */
    protected void update (float time)
    {
        // update fade, which may stop playing
        updateFade(time);
        if (_state != AL10.AL_PLAYING) {
            return;
        }

        // find out how many buffers have been played and unqueue them
        int played = _source.getBuffersProcessed();
        if (played == 0) {
            return;
        }
        for (int ii = 0; ii < played; ii++) {
            _source.unqueueBuffers(_buffers[_qidx]);
            _qidx = (_qidx + 1) % _buffers.length;
            _qlen--;
        }

        // enqueue up to the number of buffers played
        queueBuffers(played);

        // find out if we're still playing; if not and we have buffers queued, we must restart
        _state = _source.getSourceState();
        if (_qlen > 0 && _state != AL10.AL_PLAYING) {
            play();
        }
    }

    /**
     * Updates the gain of the stream according to the fade state.
     */
    protected void updateFade (float time)
    {
        if (_fadeMode == FadeMode.NONE) {
            return;
        }
        float alpha = Math.min((_fadeElapsed += time) / _fadeInterval, 1f);
        _source.setGain(_gain * (_fadeMode == FadeMode.IN ? alpha : (1f - alpha)));
        if (alpha == 1f) {
            if (_fadeMode == FadeMode.OUT) {
                stop();
            } else if (_fadeMode == FadeMode.OUT_DISPOSE) {
                dispose();
            }
            _fadeMode = FadeMode.NONE;
        }
    }

    /**
     * Queues (up to) the specified number of buffers.
     */
    protected void queueBuffers (int buffers)
    {
        for (int ii = 0; ii < buffers; ii++) {
            Buffer buffer = _buffers[(_qidx + _qlen) % _buffers.length];
            if (populateBuffer(buffer)) {
                _source.queueBuffers(buffer);
                _qlen++;
            } else {
                break;
            }
        }
    }

    /**
     * Populates the identified buffer with as much data as it can hold.
     *
     * @return true if data was read into the buffer and it should be enqueued, false if the end of
     * the stream has been reached and no data was read into the buffer
     */
    protected boolean populateBuffer (Buffer buffer)
    {
        if (_abuf == null) {
            _abuf = ByteBuffer.allocateDirect(getBufferSize()).order(ByteOrder.nativeOrder());
        }
        _abuf.clear();
        int read = 0;
        try {
            read = Math.max(populateBuffer(_abuf), 0);
        } catch (IOException e) {
            log.warning("Error reading audio stream [error=" + e + "].");
        }
        if (read <= 0) {
            return false;
        }
        _abuf.rewind().limit(read);
        buffer.setData(getFormat(), _abuf, getFrequency());
        return true;
    }

    /**
     * Returns the OpenAL audio format of the stream.
     */
    protected abstract int getFormat ();

    /**
     * Returns the stream's playback frequency in samples per second.
     */
    protected abstract int getFrequency ();

    /**
     * Populates the given buffer with audio data.
     *
     * @return the total number of bytes read into the buffer, or -1 if the end of the stream has
     * been reached
     */
    protected abstract int populateBuffer (ByteBuffer buf)
        throws IOException;

    /**
     * Returns the size in bytes of the buffers to use.
     */
    protected int getBufferSize ()
    {
        return 131072;
    }

    /**
     * Returns the number of buffers to use.
     */
    protected int getNumBuffers ()
    {
        return 4;
    }

    /** The manager to which the stream was added. */
    protected SoundManager _soundmgr;

    /** The source through which the stream plays. */
    protected Source _source;

    /** The buffers through which we cycle. */
    protected Buffer[] _buffers = new Buffer[getNumBuffers()];

    /** The starting index and length of the current queue in {@link #_buffers}. */
    protected int _qidx, _qlen;

    /** The gain of the stream. */
    protected float _gain = 1f;

    /** The interval and elapsed time for fading. */
    protected float _fadeInterval, _fadeElapsed;

    /** The type of fading being performed. */
    protected FadeMode _fadeMode = FadeMode.NONE;

    /** The buffer used to store names. */
    protected IntBuffer _nbuf;

    /** The buffer used to store audio data temporarily. */
    protected ByteBuffer _abuf;

    /** The OpenAL state of the stream. */
    protected int _state = AL10.AL_INITIAL;

    /** Fading modes. */
    protected enum FadeMode { NONE, IN, OUT, OUT_DISPOSE }
}
