//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2007 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.media.sound;

import java.util.Iterator;
import java.util.LinkedList;

import com.samskivert.util.Config;
import com.samskivert.util.RandomUtil;
import com.samskivert.util.RunQueue;

import com.threerings.media.Log;

/**
 * Manages the playing of audio files.
 */
// TODO:
//   - fade music out when stopped?
//   - be able to pause music
public class MusicManager
{
    /**
     * Constructs a music manager.
     *
     * @param smgr The soundManager we work with.
     * @param runQueue the client event run queue.
     *
     */
    public MusicManager (SoundManager smgr, RunQueue runQueue)
    {
        _smgr = smgr;
        _runQueue = runQueue;
    }

    /**
     * Shut the damn thing off.
     */
    public void shutdown ()
    {
        _musicStack.clear();
        stopMusicPlayer();
    }

    /**
     * Returns a string summarizing our volume settings and disabled sound types.
     */
    public String summarizeState ()
    {
        StringBuilder buf = new StringBuilder();
        buf.append("musicVol=").append(_musicVol);
        return buf.append("]").toString();
    }

    /**
     * Sets the volume for music.
     *
     * @param vol a volume parameter between 0f and 1f, inclusive.
     */
    public void setMusicVolume (float vol)
    {
        float oldvol = _musicVol;
        _musicVol = Math.max(0f, Math.min(1f, vol));
        if (_musicPlayer != null) {
            _musicPlayer.setVolume(_musicVol);
        }

        if ((oldvol == 0f) && (_musicVol != 0f)) {
            playTopMusic();
        } else if ((oldvol != 0f) && (_musicVol == 0f)) {
            stopMusicPlayer();
        }
    }

    /**
     * Get the music volume.
     */
    public float getMusicVolume ()
    {
        return _musicVol;
    }

    /**
     * Start playing the specified music repeatedly.
     */
    public void pushMusic (String pkgPath, String key)
    {
        pushMusic(pkgPath, key, -1);
    }

    /**
     * Start playing music for the specified number of loops. If no other
     * music is pushed, the specified music will play for the number of loops
     * specified, and will then be popped off the stack.
     */
    public void pushMusic (String pkgPath, String key, int numloops)
    {
        MusicKey mkey = new MusicKey(pkgPath, key, numloops);

        // stop any existing playing music
        if (_musicPlayer != null) {
            _musicPlayer.stop();
            handleMusicStopped();
        }

        // add the new song
        _musicStack.addFirst(mkey);

        // and play it
        playTopMusic();
    }

    /**
     * Remove the specified music from the playlist. If it is currently
     * playing, it will be stopped and the previous song will be started.
     */
    public void removeMusic (String pkgPath, String key)
    {
        MusicKey mkey = new MusicKey(pkgPath, key, -1);

        if (!_musicStack.isEmpty()) {
            MusicKey current = _musicStack.getFirst();

            // if we're currently playing this song..
            if (mkey.equals(current)) {
                // stop it
                if (_musicPlayer != null) {
                    _musicPlayer.stop();
                }

                // remove it from the stack
                _musicStack.removeFirst();
                // start playing the next..
                playTopMusic();
                return;

            } else {
                // we aren't currently playing this song. Simply remove.
                for (Iterator<MusicKey> iter=_musicStack.iterator(); iter.hasNext(); ) {
                    if (key.equals(iter.next())) {
                        iter.remove();
                        return;
                    }
                }

            }
        }

        Log.debug("Sequence stopped that wasn't in the stack anymore [key=" + mkey + "].");
    }

    /**
     * Start the specified sequence.
     */
    protected void playTopMusic ()
    {
        if (_musicStack.isEmpty()) {
            return;
        }

        // if the volume is off, we don't actually want to play anything but we want to at least 
        // decrement any loopers by one and keep them on the top of the queue
        if (_musicVol == 0f) {
            handleMusicStopped();
            return;
        }

        MusicKey info = _musicStack.getFirst();

        Config c = _smgr.getConfig(info);
        String[] names = c.getValue(info.key, (String[])null);
        if ((names == null) || (names.length == 0)) {
            Log.warning("No such music [key=" + info + "].");
            _musicStack.removeFirst();
            playTopMusic();
            return;
        }
        String music = names[RandomUtil.getInt(names.length)];

        Class playerClass = getMusicPlayerClass(music);

        // if we don't have a player for this song, play the next song
        if (playerClass == null) {
            _musicStack.removeFirst();
            playTopMusic();
            return;
        }

        // shutdown the old player if we're switching music types
        if (!playerClass.isInstance(_musicPlayer)) {
            if (_musicPlayer != null) {
                _musicPlayer.shutdown();
            }

            // set up the new player
            try {
                _musicPlayer = (MusicPlayer) playerClass.newInstance();
                _musicPlayer.init(_playerListener);

            } catch (Exception e) {
                Log.warning("Unable to instantiate music player [class=" + playerClass + 
                            ", e=" + e + "].");

                // scrap it, try again with the next song
                _musicPlayer = null;
                _musicStack.removeFirst();
                playTopMusic();
                return;
            }

            _musicPlayer.setVolume(_musicVol);
        }

        // play!
        String bundle = c.getValue("bundle", (String)null);
        try {
            // TODO: buffer for the music player?
            _musicPlayer.start(_smgr._rmgr.getResource(bundle, music));
        } catch (Exception e) {
            Log.warning("Error playing music, skipping [e=" + e +
                ", bundle=" + bundle + ", music=" + music + "].");
            _musicStack.removeFirst();
            playTopMusic();
            return;
        }
    }

    /**
     * Get the appropriate music player for the specified music file.
     */
    protected static Class getMusicPlayerClass (String path)
    {
        path = path.toLowerCase();

//         if (path.endsWith(".mid") || path.endsWith(".rmf")) {
//             return MidiPlayer.class;

//         } else if (path.endsWith(".mod")) {
//             return ModPlayer.class;

//         } else if (path.endsWith(".mp3")) {
//             return Mp3Player.class;

//         } else if (path.endsWith(".ogg")) {
//             return OggPlayer.class;

//         } else {
            return null;
//         }
    }

    /**
     * Stop whatever song is currently playing and deal with the MusicKey associated with it.
     */
    protected void handleMusicStopped ()
    {
        if (_musicStack.isEmpty()) {
            return;
        }

        // see what was playing
        MusicKey current = _musicStack.getFirst();

        // see how many times the song was to loop and act accordingly
        switch (current.loops) {
        default:
            current.loops--;
            break;

        case 1:
            // sorry charlie
            _musicStack.removeFirst();
            break;
        }
    }

    /**
     * Stop the current music player.
     */
    protected void stopMusicPlayer ()
    {
        if (_musicPlayer != null) {
            _musicPlayer.stop();
            _musicPlayer.shutdown();
            _musicPlayer = null;
        }
    }

    /**
     * A class that tracks the information about our playing music files.
     */
    protected static class MusicKey extends SoundManager.SoundKey
    {
        /** How many times to loop, or -1 for forever. */
        public int loops;

        public MusicKey (String set, String path, int loops)
        {
            super((byte) -1, set, path);
            this.loops = loops;
        }
    }

    /** The sound manager we work with. */
    protected SoundManager _smgr;

    /** The client event run queue. */
    protected RunQueue _runQueue;

    /** Volume level for music. */
    protected float _musicVol = 1f;

    /** The stack of songs that we're playing. */
    protected LinkedList<MusicKey> _musicStack = new LinkedList<MusicKey>();

    /** The current music player, if any. */
    protected MusicPlayer _musicPlayer;

    /** Event listener for receiving information about a song ending. */
    protected MusicPlayer.MusicEventListener _playerListener =
        new MusicPlayer.MusicEventListener() {
            public void musicStopped () {
                _runQueue.postRunnable(new Runnable() {
                    public void run() {
                        handleMusicStopped();
                        playTopMusic();
                    }
                });
            }
        };
}
