package speechrecorder;


import java.awt.Container;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import javax.sound.sampled.*;
import javax.swing.*;


/**
 * @author  kmaclean
 */
public class PlayerApplet extends JApplet {

    private Downloader dl;
    private Playback pl;
 // !!!!!!   
 //   private static final boolean DEBUG = false; // Whether to output messages to the console
    private static final boolean DEBUG = true; // Whether to output messages to the console
    
// !!!!!!    
    private URL url; // The URL as supplied via applet parameter
    private File tmpFile; // The remote data is downloaded and stored here
    private long fileSize; // The filesize, irrespective of how much is downloaded
    private long bytesDownloaded = 0L;
    
    private String cookie;
    
    private InputStream urlInputStream;
    private OutputStream fileOutStream;
    private InputStream fileInputStream;
    private AudioInputStream audioInputStream;
    private AudioFormat audioFormat;
    private AudioFormat targetFormat;
    private SourceDataLine sourceDataLine;
    
    private JProgressBar progressBar;
    private JSlider slider;
    private JButton playBut;

    private boolean headerBytesLoaded = false; // Ogg Speex takes 108 (28+80) bytes for header
    private boolean audioInitialised = false;

    private boolean isPlaying = false;

    public void init(){
        getParameters();
        initialiseStreams();
        layoutGui();
        
        dl = new Downloader();
        dl.start();
        pl = new Playback();
    }

    /**
    * May halt applet if required param (file) does not exist
    */
    private void getParameters(){
        try {
// !!!!!!        	
 //           String fileParam = getParameter("file");
            url = new URL("http://localhost:90/httpd/audiosubmissions/cc-01.wav");
// !!!!!!            
        } catch (NullPointerException err){
            reportError("\"file\" parameter is required - not found", err);
            System.exit(1);
        } catch (MalformedURLException err){
            reportError("\"file\" parameter is not a correctly-formed URL", err);
            System.exit(1);
        }

        try {
            cookie = getParameter("cookie");
        } catch (NullPointerException err){
            reportError("\"cookie\" parameter not found - this may cause problems trying to read the audio file", err);
            cookie = null;
        }

    }
    
    /**
    * Set up download, cache writing, and cache reading streams.
    * May halt applet if anything cannot be opened
    */
    private void initialiseStreams(){
        
        try {
            URLConnection conn = url.openConnection();
            
            // The cookie value should be a parameter passed in to the applet
            if(cookie != null) {
                conn.setRequestProperty("Cookie", cookie);
            }
            
            conn.connect();
            urlInputStream = new BufferedInputStream(conn.getInputStream());
            fileSize = conn.getContentLength();
            
            if(DEBUG){
                if(fileSize == -1){
                    System.err.println("initialiseStreams(): File size cannot be determined for the remote file, which is a shame.");
                } else {
                    System.err.println("initialiseStreams(): File size for the remote file is " + fileSize);
                }
            }
            
        } catch (IOException err){
            reportError("Unable to get an input stream from specified URL", err);
            System.exit(1);
        }
        
        try {
            tmpFile = File.createTempFile("mdl", ".spx");
            tmpFile.deleteOnExit();
System.err.println("MSPA tmpFile = " + tmpFile);
        } catch (IOException err){
            reportError("Unable to create temp file for storing data", err);
            System.exit(1);
        }
        
        try {
            fileOutStream = new BufferedOutputStream(
                 new FileOutputStream(tmpFile));
        } catch (Exception err){
            reportError("Unable to open stream for writing audio data to temp file", err);
            System.exit(1);
        }
        
        try {
            fileInputStream = new BufferedInputStream(new FileInputStream(tmpFile));
        } catch (IOException err){
            reportError("Unable to get an input stream to read the temp file's audio data", err);
            System.exit(1);
        }
        
    }
    
    /**
    * Create GUI elements
    */
    private void layoutGui(){

       Container pane = getContentPane();
       
       pane.setLayout(new BoxLayout(pane, BoxLayout.X_AXIS));

       playBut = new JButton("Play");
       playBut.setEnabled(false);
       playBut.addActionListener(new ActionListener(){
                       public void actionPerformed(ActionEvent e){
                           if(playBut.getText().equals("Stop")){
                               pl.stopPlayback();
                           }else{
                               pl.startPlayback();
                           }
                       }
                       });
       
       progressBar = new JProgressBar();
       progressBar.setString("Loading");
       progressBar.setStringPainted(true);
       progressBar.setIndeterminate(fileSize == -1);
       
       pane.add(playBut);
       pane.add(progressBar);
    }
    
    /**
    * Called by the download thread when enough file has downloaded to be able 
    * to interpret the audio format from the header
    */
    private void initialiseAudio(){
        
        
        try{
            fileInputStream = new BufferedInputStream(new FileInputStream(tmpFile));
            
       
            AudioFileFormat aff = AudioSystem.getAudioFileFormat(fileInputStream);
            
            
            
            
            System.err.println("AudioFileFormat derived from stream is: " + aff);
            System.err.println("AudioFileFormat.Type is: " + aff.getType());
            System.err.println("AudioFormat is: " + aff.getFormat());
            
            audioFormat = aff.getFormat();
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);


            if(DEBUG){
                if (!AudioSystem.isLineSupported(info)) {
                    System.err.println("AudioSystem does not support this DataLine.Info (made out of the AudioFormat): " + info);
                } else {
                    System.err.println("HOORAH! AudioSystem does support this DataLine.Info (made out of the AudioFormat): " + info);
                }
            }
//          !!!!!!
/*             targetFormat = new AudioFormat(
                AudioFormat.Encoding.PCM_SIGNED,
                audioFormat.getSampleRate(),
                16,
                audioFormat.getChannels(),
                audioFormat.getChannels() * 2,
                audioFormat.getSampleRate(),
              false);
*/              
//            audioInputStream = new org.xiph.speex.spi.Speex2PcmAudioInputStream(fileInputStream, targetFormat, AudioSystem.NOT_SPECIFIED);
            audioInputStream = AudioSystem.getAudioInputStream(fileInputStream);
// !!!!!!            
            if(DEBUG){
                System.err.println("--- successfully grabbed audioInputStream by direct rather than SPI ---");
            }

        } catch (IOException err) {
            reportError("IO error while trying to open cache file for playback", err);
            return;
        } catch(UnsupportedAudioFileException err) {
            reportError("'Unsupported audio file' error while trying to open cache file for playback", err);
            return;
        }
        
        audioFormat = audioInputStream.getFormat();

        DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
        // If the audioFormat is not directly supported
        if (!AudioSystem.isLineSupported(info)) {
          AudioFormat sourceFormat = audioFormat;
// !!!!!!! not required
/*          AudioFormat targetFormat = new AudioFormat(
                AudioFormat.Encoding.PCM_SIGNED,
                sourceFormat.getSampleRate(),
                16,
                sourceFormat.getChannels(),
                sourceFormat.getChannels() * 2,
                sourceFormat.getSampleRate(),
                false);
                
      
          audioInputStream = AudioSystem.getAudioInputStream(targetFormat, audioInputStream);
          audioFormat = audioInputStream.getFormat();

          info = new DataLine.Info(SourceDataLine.class, audioFormat);
*/
//        !!!!!!! 
        }
       
        
        try {
          sourceDataLine = (SourceDataLine) AudioSystem.getLine(info);
          // We have to open the line for it to be ready to receive audio data.
          sourceDataLine.open(audioFormat);
        }
        catch (Exception err) {
          reportError("Unable to start the audio output based on the required audio parameters.", err);
          return;
        }


        // Once init is complete, then playback can be allowed
        audioInitialised = true;
        playBut.setEnabled(true);
    }
    
    /**
    * This seems to be the best way to "rewind" the stream to the beginning
    * after the audio has played (use of "mark/reset" is problematic). You'd
    * think there'd be a better way than closing and reopening. 
    * And maybe there is.
    */
    private void closeAndReopenStream(){
        try{
            audioInputStream.close();
            // These functions are the same as the initialiseAudio() func
            //  except we don't need to re-calculate the stream types
            fileInputStream = new BufferedInputStream(new FileInputStream(tmpFile));
// !!!!!!            
  //          audioInputStream = new org.xiph.speex.spi.Speex2PcmAudioInputStream(fileInputStream, targetFormat, AudioSystem.NOT_SPECIFIED);
            audioInputStream = AudioSystem.getAudioInputStream(fileInputStream);
//          !!!!!!                
        } catch (IOException err) {
            reportError("IO error while trying to RE-open cache file for playback", err);
            return;
// !!!!!!            
        }catch (UnsupportedAudioFileException err) {
            reportError("UnsupportedAudioFileException error while trying to RE-open cache file for playback", err);
            return;
        }
// !!!!!!        
    }
    
    /**
    * Used when fatal errors occur
    */
    private void reportError(final String str, final Exception err){
        System.err.println("PlayerApplet important error: " + str);
        err.printStackTrace();
        JButton errButt = new JButton("Error");
        errButt.addActionListener(new ActionListener(){
                       public void actionPerformed(ActionEvent e){
                         JOptionPane.showMessageDialog(null, str + "\n\n" + err, "PlayerApplet error", JOptionPane.ERROR_MESSAGE);
                       }
                       });
        getContentPane().add(errButt);
    }
    
    
    
    /**
    * Called by the playback thread to alert the applet when playback stops
    */
    private void playbackHasStopped(){
       playBut.setText("Play");
    }
    
    /**
    * Called by the downloader when the amount changes.
    * Might be nice to update this not quite as often as every dnld chunk.
    */
    private void updateDownloadProgress(){
       progressBar.setValue((int)(fractionDownloaded() * 100.0));
    }

    
    /**
    * If fileSize==-1, will always return zero, since there's no way to know.
    * Otherwise returns a float between zero and one.
    */
    float fractionDownloaded(){
        if(fileSize==-1){
            return 0.0f;
        } else {
            return ((float)bytesDownloaded) / (float)fileSize;
        }
    }
    


    ////////////////////// Playback thread class //////////////////////

    class Playback implements Runnable {
        long bytePos = 0L;
        protected Thread thread;
    
        void startPlayback(){
            if(DEBUG){
                System.err.println("startPlayback() called");
            }
            
            if(isPlaying){
                if(DEBUG){
                    System.err.println("startPlayback() - isPlaying so not doing anything");
                }
                return;
            }

            playBut.setText("Stop");
            
            isPlaying = true;

            thread = new Thread(this);
            thread.setName("Playback");
            thread.start();
        }
        
        void stopPlayback(){
            if(DEBUG){
                System.err.println("stopPlayback() called");
            }
            isPlaying = false;
        }
        
        public void run(){
            //byte[] buffer = new byte[16384];
            byte[] buffer = new byte[4096]; // Smallish to make the "stop" button respond quicker
            int bytesRead;
            int totalBytesRead=0;            
            
            if(!audioInitialised) { // This should never happen, really - play button only active after audio is inited
                initialiseAudio();
            }

            closeAndReopenStream(); // This is required for repeated presses of the Play button to work
            
            sourceDataLine.start();
            
            try {
                while(isPlaying && ((bytesRead = audioInputStream.read(buffer)) != -1 )){
                    sourceDataLine.write(buffer, 0, bytesRead);
                    totalBytesRead += bytesRead;
//System.err.println("Playback thread: read/written "+bytesRead+" bytes. Total: " + totalBytesRead + " bytes. Available: "+audioInputStream.available());


                    // You'll have 0 bytes available if the buffers are still filling,
                    //  or (seemingly) when the file has ended. The JSpeex libraries
                    //  don't seem to be dealing with EOF in any sensible way.
                    // They should return -1 rather than blocking indefinitely!
                    // I hope that "break"ing here doesn't cause problems by
                    //  stopping play prematurely - doesn't on my system but
                    //  maybe on lower-spec systems...?
                    if(audioInputStream.available()==0) {
                      break;
                    }
                }
            } catch (IOException err) {
                reportError("I/O error in playback thread, while trying to write audio data to output", err);
            }

            if(DEBUG){
                System.err.println("Playback thread: finished - about to drain&stop.");
            }
            
            sourceDataLine.drain(); // Drain the buffer - otherwise the end of the file may never be played
            // DON'T STOP - MAY NOT BE ABLE TO PLAY MORE IN FUTURE      sourceDataLine.stop();

            if(DEBUG){
                System.err.println("Playback thread: drained & stopped.");
            }

            playbackHasStopped();
            isPlaying = false;
        }
        
    } // End class Playback
    
    
    
    ////////////////////// Downloader thread class //////////////////////
    
    /**
	 * @author  kmaclean
	 */
    class Downloader extends Thread {
    
        boolean isComplete = false;
    
        /**
		 * @return
		 * @uml.property  name="isComplete"
		 */
        boolean isComplete(){
            return isComplete;
        }
        
        public void run(){
            int numBytes = 0;
            byte[] byteData = new byte[4096]; // Is this a sensible size? Check later.
          
            try{
                while((numBytes = urlInputStream.read(byteData, 0, byteData.length)) != -1){
                    fileOutStream.write(byteData, 0, numBytes);
                    bytesDownloaded += numBytes;
                    if(fileSize != -1){
                        updateDownloadProgress();
                    }
                    if(numBytes==0){ // Nothing available just yet so...
                        yield(); // ...let other threads have a go with the CPU
                    }else{
//                         if((!headerBytesLoaded) && (bytesDownloaded > 4096)){
/*
                         if((!headerBytesLoaded) && (tmpFile.length() > 4096)){
                             // The amount required above is more than really needed (108 bytes is size of header)
                             //   but should make very little difference. A bit of padding for safety.
                             //   What's the smallest theoretical Speex file...? Very small!
                             headerBytesLoaded = true;
                             if(DEBUG){
                                 System.err.println("Assuming Ogg Speex header is readable now");
                             }
                             
                             // Trigger the audio format to be interpreted
                             initialiseAudio();
                             
                         }
*/
                    }
                    if(DEBUG){
                        System.err.println("Downloader.run(): bytesDownloaded="+bytesDownloaded+", numBytes="+numBytes);
                    }
                }
                fileOutStream.flush();
                fileOutStream.close();
                if(DEBUG){
                    System.err.println("Downloader.run(): COMPLETED!");
                }

                // This should ideally be run earlier than 100% completion.
                // Should only be triggered here if the file is too small to 
                //  triggered by the check that occurs during the loop above.
                if(!headerBytesLoaded) {
                    initialiseAudio();
                }


                isComplete = true;
                progressBar.setString("Loaded");
                progressBar.setIndeterminate(false);
                progressBar.setValue(100);
                try{
                    Thread.sleep(2000L);
                    progressBar.setString("");
                }catch(InterruptedException err){
                }
            }catch(IOException err){
                reportError("Downloader.run() encountered IOException while downloading to cache. bytesDownloaded="+bytesDownloaded+", numBytes="+numBytes, err);
            }
        }
    } // End class Downloader


} // End main class