/*
Copyright (c) 2006, Geomatics and Cartographic Research Centre, Carleton 
University All rights reserved.

Redistribution and use in source and binary forms, with or without 
modification, are permitted provided that the following conditions are met:

 - Redistributions of source code must retain the above copyright notice, 
   this list of conditions and the following disclaimer.
 - Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 - Neither the name of the Geomatics and Cartographic Research Centre, 
   Carleton University nor the names of its contributors may be used to 
   endorse or promote products derived from this software without specific 
   prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.

$Id$
*/
package ca.carleton.gcrc.atlas;

import java.io.IOException;

import java.net.URL;
import java.net.MalformedURLException;

import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioFormat;

public class StreamTrack extends AudioTrack implements LineListener {

	private PlayThread m_playThread;
	private boolean m_reaping;
	private boolean m_playThreadActiveFlag;
	private AudioInputStream m_secondInputStream;
	protected ReloadThread m_reloadThread;

	final int bufsize = 128000;
	protected byte[] m_secondBuffer = null; // new byte[bufsize];
	protected int m_secondBytesRead = -1;

	private boolean m_streamCompleted;

	public StreamTrack(URL url_, String origUrlString_) {
		super(url_, origUrlString_);
		m_reaping = false; 					// not yet killing the playThread
		m_playThreadActiveFlag = false; 	// not yet ready for the playThread to do anything
		m_playThread = new PlayThread(this);
		m_playThread.setPriority(m_playThread.getPriority() + 2); // play thread - higher priority
		m_secondInputStream = null;
		m_reloadThread = new ReloadThread(this);
		m_reloadThread.setPriority(m_reloadThread.getPriority() - 2); // reload thread - lower priority
		m_streamCompleted = false;			// have not detected that the playThread has gotten to end of file
	}

	public static void main(String[] args) {
		AudioTrack soundtest;
		URL url;

		try {
			// url = new URL("http://devel0.gcrc.carleton.ca/~amoshayes/test.au");
			url = new URL("http://devel0.gcrc.carleton.ca/~glennbrauen/eno_ambient1_track2.ogg");
			System.out.println("Loading Sound...");
			soundtest = new StreamTrack(url, null);
			soundtest.setShouldLoop(true);
			soundtest.setGain((float) 1.0);
			soundtest.setPlaying(true);
			System.out.println("Sound played.");
		} catch (MalformedURLException e) {
			System.out.println("Error.");
			e.printStackTrace();
		}
	}

	protected void reloadSound() {
		try {
			if (null != m_secondInputStream) {
				m_secondInputStream.close();
				m_secondInputStream = null;
			}
			m_secondInputStream = getAndConvertAudioInputStream(); // for looping
		} catch (Exception ex) {
			//do nothing
		}
	}
	
	protected void loadSound() {
		// This runs on the LoadThread thread. Needs to be called from the LoadThread class but not from outside.
		
		SourceDataLine sdl;
//		long frameLength;
		
		m_inputStream = getAndConvertAudioInputStream();
		
		if (null != m_inputStream) {
//			try {
//				// Get total length in bytes of the encoded stream.  Note it will probably be -1.
//				DebugLog("loadSound: getting frameLength.");
//				frameLength = m_inputStream.getFrameLength();
//				DebugLog("loadSound: " + m_url + " frame length: " + frameLength);
//			}
//			catch (Exception ex) {
//				DebugLog("loadSound:Cannot get m_encodedaudioInputStream.getFrameLength()");
//			}
			
			try { // open the output datataline.  InputStream not modified here...
				DataLine.Info info = new DataLine.Info(SourceDataLine.class,
						m_inputStream.getFormat(), AudioSystem.NOT_SPECIFIED);

				m_currentDataLine = (SourceDataLine) AudioSystem.getLine(info);
				DebugLog("StreamTrack::loadSound:sdl = " + m_currentDataLine.toString());
				DebugLog("StreamTrack::loadSound:sdl.format = " + m_currentDataLine.getFormat().toString());
				
				int buffersize = m_currentDataLine.getBufferSize();
				DebugLog("StreamTrack::loadSound:buffersize = " + buffersize, Debug.detailLevel);
				
				sdl = (SourceDataLine) m_currentDataLine;
				sdl.open(m_inputStream.getFormat(), buffersize);
				
				m_currentDataLine.addLineListener(this);
				m_gainControl =
					(FloatControl) m_currentDataLine.getControl(FloatControl.Type.MASTER_GAIN);
				
				m_reloadThread.signalLoopRestart(); // signal loop start - loads loop restart buffer
			} catch (Exception ex) {
				ex.printStackTrace();
				return;
			}
			changeGain();
			playSourceLine();
		}
	} // end loadSound()
	
	protected void playSourceLine() {
		if (true == m_isPlaying && false == m_isPaused) {
			m_playThreadActiveFlag = true; // tell play thread to start reading and writing data
		} else { // Not playing
			m_playThreadActiveFlag = false; // make it stop
		}
	}
	
	protected int computeReadBufferSize() {
		int retVal;
		
		if (null != m_inputStream) {
			AudioFormat format = m_inputStream.getFormat();
			if (null != format) {
				float oneSecondBufSize = format.getFrameSize() * format.getFrameRate(); // think this includes number of channels in framesize.
				float quarterSecondBufSize = oneSecondBufSize / 4;
				retVal = (int)(quarterSecondBufSize > bufsize ? bufsize : (int) quarterSecondBufSize);
				float mod = retVal % format.getFrameSize();
				if (mod != 0) {
					retVal -= mod; // writes must be integral number of frames.
				}
				DebugLog("computeReadBufferSize: calculated quarter second read buffer size: " + retVal, Debug.detailLevel);
			} else {
				retVal = bufsize;
				DebugLog("computeReadBufferSize: can't access input stream format - use default buffer size: " + retVal);
			}
		} else {
			retVal = bufsize;
			DebugLog("computeReadBufferSize: can't access input stream - use default buffer size: " + retVal);
		}
		
		return (retVal);
	} // end computeReadBufferSize()

	/**
	 * Public LineListener interface: Process line event updates generated against
	 * the underlying SourceDataLine (m_currentDataLine).  In particular, process
	 * stop events generated by the PlayThread in the event that the input stream
	 * reaches end-of-media and looping has NOT been turned on (PlayThread HIDES the
	 * end-of-media condition if looping has been turned on).  In the case of a STOP
	 * event generated by end-of-media when not looping, this function recreates the
	 * input stream in case a subsequent command is received to replay the track.
	 * 
	 * @param le the received line event.
	 */
	public void update(LineEvent le) {
		DebugLog("StreamTrack::update: event - " + le.toString() + " url: " + m_url);
		if (le.getType().equals(LineEvent.Type.STOP)) {
			if (true == m_streamCompleted) { // the play thread declared that it reached end of file.
				m_isPlaying = false;         // update track status - no longer playing if end-of-media detected by play thread.
				if (null != m_inputStream) {
					try {
						m_inputStream.close();
						m_inputStream = null;
					} catch (Exception ex) {
						//do nothing
					}
				}
				m_inputStream = getAndConvertAudioInputStream();
				m_streamCompleted = false;
			} // end if (play thread declared end-of-media reached)
		} // end if (stop event)
	} // end update()
	
	public void destroy() {
		m_reaping = true;
		if (null != m_playThread) {
			try {
				m_playThread.join();
			} catch (InterruptedException e) {
			}
		}
		if (null != m_reloadThread) {
			try {
				m_reloadThread.join();
			} catch (InterruptedException e) {
			}
		}
		if (null== m_inputStream) {
			try {
				m_inputStream.close();
				m_inputStream = null;
			} catch (Exception ex) {
				//do nothing
			}
		}
		if (null== m_secondInputStream) {
			try {
				m_secondInputStream.close();
				m_secondInputStream = null;
			} catch (Exception ex) {
				//do nothing
			}
		}
		m_currentDataLine.removeLineListener(this);
		super.destroy();
	} // destroy()
	
	class ReloadThread extends Thread {
		StreamTrack m_parentTrack;
		boolean m_loopSignal = false;
		
		public ReloadThread(StreamTrack parentTrack_) {
			m_parentTrack = parentTrack_;
			start();
		}
		
		public void signalLoopRestart() {
			m_loopSignal = true;
		}
		
		public void run() {
			/*
			 * start-up: park until the load thread completes... check that it seems to have worked.
			 */
			while (isLoadThreadRunning()) {
				try {
					sleep(200);
				} catch (Exception ex) {
				}
			}
			if (!hasLoadThreadCompletedNormally()) {
				// no point trying to play it if the load thread does appear to have
				// been successful.
				return;
			}

			while (false == m_parentTrack.m_reaping) {
				if (true == m_loopSignal) {
					m_parentTrack.reloadSound();
					m_loopSignal = false;
					int retryCount = 0;
					
					if (null != m_parentTrack.m_secondInputStream) { // fill the second buffer - retry a few times if necessary
						int bytesRead_temp;
						int lengthToRead_temp;
						m_parentTrack.m_secondBytesRead = 0;
						lengthToRead_temp = m_parentTrack.computeReadBufferSize();
						if (null == m_secondBuffer) {
							m_secondBuffer = new byte[lengthToRead_temp];
						}
						do {
							try {
								bytesRead_temp = m_parentTrack.m_secondInputStream.read(m_parentTrack.m_secondBuffer,
										m_parentTrack.m_secondBytesRead, lengthToRead_temp);
								if (bytesRead_temp > 0) {
									m_parentTrack.m_secondBytesRead += bytesRead_temp;
									lengthToRead_temp -= bytesRead_temp;
								} else if (bytesRead_temp == -1 && 0 == m_parentTrack.m_secondBytesRead) { // this is bad! restart process!
									++retryCount;
									lengthToRead_temp = 0;
									m_loopSignal = true;
									DebugLog("ReloadThread: stream closed!  Retrying.");
								}
							} catch (IOException e) {
								e.printStackTrace();
							}
							try {
								sleep(100);
							} catch (Exception ex) {
							}
						} while (lengthToRead_temp > 0 && false == m_parentTrack.m_reaping && retryCount < 10);
						DebugLog("reloadThread - bytes read: " + m_parentTrack.m_secondBytesRead, Debug.mediumLevel);
					}
				}
				try {
					sleep(200); // in millis - presumably using a stream track because audio has a significant play length!
				} catch (Exception ex) {
				}
			}
		}
	} // end of ReloadThread class

	class PlayThread extends Thread {
		StreamTrack m_parentTrack;
		
		public PlayThread(StreamTrack parentTrack_) {
			m_parentTrack = parentTrack_;
			start();
		}
		
		public void run() {
			int nBytesRead = 0;
			int nBytesWritten = 0;
			SourceDataLine sdl;
			AudioInputStream tempStream;
			boolean reenteringReadWriteLoop;
			int readBufSize = 0;
			byte[] inputStreamData = null;
			
			/*
			 * start-up: park until the load thread completes... check that it seems to have worked.
			 */
			while (isLoadThreadRunning()) {
				try {
					sleep(200);
				} catch (Exception ex) {
				}
			}
			if (!hasLoadThreadCompletedNormally()) {
				// no point trying to play it if the load thread does appear to have
				// been successful.
				return;
			}

			while (false == m_parentTrack.m_reaping) {

				reenteringReadWriteLoop = true;
				sdl = null;
				while (true == m_parentTrack.m_playThreadActiveFlag &&		// parent says we're active
					   false == m_parentTrack.m_reaping) {					// parent isn't trying to clean up
					sdl = (SourceDataLine) m_parentTrack.m_currentDataLine;
					
					if (true == reenteringReadWriteLoop) { // we've just come into the inner loop
						if (null == inputStreamData) { // first time only
							readBufSize = m_parentTrack.computeReadBufferSize();
							inputStreamData = new byte[readBufSize];
						}
						if (false == sdl.isActive()) {
							sdl.start();
						}
						reenteringReadWriteLoop = false;
					}
										
					try {
						nBytesRead = m_parentTrack.m_inputStream.read(inputStreamData, 0, inputStreamData.length);			
					} catch (IOException e) {
						e.printStackTrace();
					}
					if (nBytesRead >= 0) {
						nBytesWritten = sdl.write(inputStreamData, 0, nBytesRead);
						DebugLog("bytes read: " + nBytesRead + " bytes written: " + nBytesWritten, Debug.detailLevel);
					} else if (nBytesRead == -1) { // end of file
						if (true == m_parentTrack.m_shouldLoop) {
							if (null != m_parentTrack.m_secondInputStream) {
								nBytesWritten = sdl.write(m_parentTrack.m_secondBuffer, 0, m_parentTrack.m_secondBytesRead);
								DebugLog("bytes read: " + m_parentTrack.m_secondBytesRead + " bytes written: "
										+ nBytesWritten, Debug.detailLevel);
								tempStream = m_parentTrack.m_inputStream; // switch first and second streams and then reload
								m_parentTrack.m_inputStream = m_parentTrack.m_secondInputStream;
								m_parentTrack.m_secondInputStream = tempStream;
							}
							if (null != m_parentTrack.m_reloadThread) {
								m_parentTrack.m_reloadThread.signalLoopRestart();
							}
						} else {
							DebugLog("bytes read: " + nBytesRead, Debug.detailLevel);							
							m_parentTrack.m_streamCompleted = true;       // signal end-of-media case.
							m_parentTrack.m_playThreadActiveFlag = false; // break out of this read/write loop
						}
					}
				} // end while (active)
				
				if (false == reenteringReadWriteLoop) { // first pass since executing inner loop - flag reset at top of loop
					if (true == m_parentTrack.m_streamCompleted) {
						/*
						 * just exited the read/write loop and the end-of-media condition was signalled.
						 * ... therefore not looping and end-of-media.  Drain the output line before stopping it.
						 * 
						 * note: this blocks until drained - fortunately i/o is all single-threaded and is done in the loop (above)
						 * so no other data can be written to this line while this thread is blocked.
						 */ 
						sdl.drain();  
					}
					sdl.stop();
				}
								
				try {
					sleep(200);
				} catch (Exception ex) {
				}
			} // end while (! being reaped)
		} // end run()
	} // end PlayThread class
}
