/**
 * 11/19/04     1.0 moved to LGPL. 
 *-----------------------------------------------------------------------
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as published
 *   by the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program 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 Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *----------------------------------------------------------------------
 */

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;

import javazoom.jl.decoder.Bitstream;
import javazoom.jl.decoder.BitstreamException;
import javazoom.jl.decoder.Decoder;
import javazoom.jl.decoder.Header;
import javazoom.jl.decoder.JavaLayerException;
import javazoom.jl.decoder.SampleBuffer;
import javazoom.jl.player.AudioDevice;
import javazoom.jl.player.FactoryRegistry;

/**
 * Play music files.
 * This class is a modified version of javazoom.jl.player.advanced.AdvancedPlayer,
 * which is part of the javazoom JLayer library.
 * The main modifications consist of:
 *     + Restriction to playing files rather than streams.
 *     + Pre-reading of the audio file to determine its length in frames.
 * These modifications permit arbitrary seek operations.
 * 
 * Modifications by David J. Barnes and Michael Kölling.
 * @version 2011.07.31
 * This class is not suitable for playing streams as a file is read
 * completely before playing.
 */
public class MusicFilePlayer
{
    // The MPEG audio bitstream.
    private Bitstream bitstream;
    // The MPEG audio decoder.
    private Decoder decoder;
    // The AudioDevice the audio samples are written to.
    private AudioDevice audio;
    // Whether currently playing.
    private boolean playing = false;
    // The file being played.
    private String filename;
    
    // The number of frames.
    private int frameCount;
    // The current frame number.
    private int frameNumber;
    // The position to resume, if any.
    private int resumePosition;

    /**
     * Creates a new MusicFilePlayer instance.
     * @param filename The file to be played.
     */
    public MusicFilePlayer(String filename) throws JavaLayerException
    {
        this.filename = filename;
                
        openAudio();
 
        frameCount = getFrameCount(filename);
        
        // Open a fresh bitstream following the frame count.
        openBitstream(filename);
        
        frameNumber = 0;
        resumePosition = -1;  

    }

    /**
     * Play the whole file.
     */
    public void play() throws JavaLayerException
    {
        playFrames(0, frameCount);
    }

    /**
     * Plays a number of MPEG audio frames.
     *
     * @param frames    The number of frames to play.
     * @return  true if the last frame was played, or false if there are
     *          more frames.
     */
    public boolean play(int frames) throws JavaLayerException
    {
        return playFrames(frameNumber, frameNumber + frames);

    }

    /**
     * Plays a range of MPEG audio frames
     * @param start The first frame to play
     * @param end       The last frame to play
     * @return true if the last frame was played, or false if there are more frames.
     */
    public boolean play(int start, int end) throws JavaLayerException
    {
        return playFrames(start, start + end);
    }
    
    /**
     * Play from the given position to the end.
     * @param start The first frame to play.
     * @return true if the last frame was played, or false if there are more frames.
     */
    public boolean playFrom(int start) throws JavaLayerException
    {
        return playFrames(start, frameCount);
    }
    
    /**
     * Get the length of the file (in frames).
     * @return The file length, in frames.
     */
    public int getLength()
    {
        return frameCount;
    }
    
    /**
     * Get the current playing position (in frames).
     * @return The current frame number.
     */
    public int getPosition()
    {
        return frameNumber;
    }
    
    /**
     * Set the playing position (in frames).
     * Playing does not start until resume() is called.
     * @param position The playing position.
     */
    public void setPosition(int position) throws JavaLayerException
    {
        pause();
        resumePosition = position;
    }
    
    
    /**
     * Pause the playing.
     */
    public void pause() throws JavaLayerException
    {
        synchronized(this) {
            playing = false;
            resumePosition = frameNumber;
        }
    }
    
    /**
     * Resume the playing.
     */
    public void resume() throws JavaLayerException
    {
        if(!playing) {
            int start;
            if(resumePosition >= 0) {
                start = resumePosition;
            }
            else {
                start = frameNumber;
            }
            resumePosition = -1;
            playFrames(start, frameCount);
        }
    }
    
    /**
     * Return the current frame number.
     * @return The number of the last frame played, or -1 if nothing played yet.
     */
    public int getFrameNumber()
    {
        return frameNumber;
    }
    
    /**
     * Play the number of frames left.
     * @return true If finished for any reason, false if paused.
     */
    private boolean playFrames(int start, int end) throws JavaLayerException
    {
        // Clear any resumption position.
        resumePosition = -1;
        
        if(end > frameCount) {
            end = frameCount;
        }
        
        // Make sure the player is in the correct position in the input.
        synchronized(this) {
            moveTo(start);
            playing = true;
        }

        // Play until finished, paused, or a problem.
        boolean ok = true;
        while (frameNumber < end && playing && ok) {
            ok = decodeFrame();
            if(ok) {
                frameNumber++;
            }                    
        }

        // Stopped for some reason.
        synchronized(this) {
            playing = false;
            // last frame, ensure all data flushed to the audio device.
            AudioDevice out = audio;
            if (out != null) {
                out.flush();
            }
        }
        return ok;
    }
    
    /**
     * Set the playing position.
     * @param position (in frames)
     */
    private void moveTo(int position) throws JavaLayerException
    {
        if(position < frameNumber) {
            synchronized(this) {
                // Already played too far.
                if(bitstream != null) {
                    try {
                        bitstream.close();
                    }
                    catch (BitstreamException ex) {
                    }
                }
                if(audio != null) {
                    audio.close();
                }
                openAudio();
                openBitstream(filename);
                frameNumber = 0;
            }
        }
        
        while(frameNumber < position) {
            skipFrame();
            frameNumber++;
        }            
    }

    /**
     * Cloases this player. Any audio currently playing is stopped
     * immediately.
     */
    public void close()
    {
        synchronized(this) {
            if (audio != null) {
                AudioDevice out = audio;
                audio = null;
                // this may fail, so ensure object state is set up before
                // calling this method.
                out.close();
                try {
                    bitstream.close();
                }
                catch (BitstreamException ex) {
                }
                bitstream = null;
                decoder = null;
            }
        }
    }

    /**
     * Decodes a single frame.
     *
     * @return true if there are no more frames to decode, false otherwise.
     */
    protected boolean decodeFrame() throws JavaLayerException
    {
        try
        {
            synchronized (this) {
                if (audio == null) {
                    return false;
                }
    
                Header h = readFrame();
                if (h == null) {
                    return false;
                }
    
                // sample buffer set when decoder constructed
                SampleBuffer output = (SampleBuffer) decoder.decodeFrame(h, bitstream);

                if(audio != null) {
                    audio.write(output.getBuffer(), 0, output.getBufferLength());
                }
            }

            bitstream.closeFrame();
        }
        catch (RuntimeException ex) {
            ex.printStackTrace();
            throw new JavaLayerException("Exception decoding audio frame", ex);
        }
        return true;
    }


    /**
     * skips over a single frame
     * @return false    if there are no more frames to decode, true otherwise.
     */
    protected boolean skipFrame() throws JavaLayerException
    {
        Header h = readFrame();
        if (h == null) {
            return false;
        }
        frameNumber++;
        bitstream.closeFrame();
        return true;
    }

    /**
     * closes the player and notifies <code>PlaybackListener</code>
     */
    public void stop()
    {
        close();
    }
    
    /**
     * Count the number of frames in the file.
     * This can be used for positioning.
     * @param filename The file to be measured.
     * @return The number of frames.
     */
    protected int getFrameCount(String filename) throws JavaLayerException
    {
        openBitstream(filename);
        int count = 0;
        while(skipFrame()) {
            count++;
        }
        bitstream.close();
        return count;        
    }
    
    /**
     * Read a frame.
     * @return The frame read.
     */
    protected Header readFrame() throws JavaLayerException
    {
        if(audio != null) {
            return bitstream.readFrame();
        }
        else {
            return null;
        }
    }
    
    /**
     * Open an audio device.
     */
    protected void openAudio() throws JavaLayerException
    {
        audio = FactoryRegistry.systemRegistry().createAudioDevice();
        decoder = new Decoder();
        audio.open(decoder);
    }
    
    /**
     * Open a BitStream for the given file.
     * @param filename The file to be opened.
     * @throws IOException If the file cannot be opened.
     */
    protected void openBitstream(String filename)
        throws JavaLayerException
    {
        try {
            bitstream = new Bitstream(new BufferedInputStream(
                        new FileInputStream(filename)));
        }
        catch(java.io.IOException ex) {
            throw new JavaLayerException(ex.getMessage(), ex);
        }
                    
    }
}



