/*
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.net.URL;
import java.util.Iterator;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.UnsupportedAudioFileException;

import javazoom.spi.vorbis.sampled.convert.VorbisFormatConversionProvider;
import javazoom.spi.vorbis.sampled.file.VorbisAudioFileReader;
import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;

public abstract class AudioTrack {
	
	protected DataLine m_currentDataLine; //input data line - specialized in AudioTrack subclasses
	protected FloatControl m_gainControl;	
	protected URL m_url;
	private String m_origUrlString; // original string given to track manager - this is in his map.
	protected boolean m_isPaused;
	protected boolean m_isPlaying;
	protected boolean m_shouldLoop;
	private float m_gain;
	private float m_configuredGain;
	private LoadThread m_loadThread;
	protected boolean m_unloadWhenFaded;
	private boolean m_muted;
	protected AudioInputStream m_inputStream;
	
	public AudioTrack(URL url_, String origUrlString_) {
		m_url = url_;
		m_origUrlString = origUrlString_;
		m_currentDataLine = null;
		m_gainControl = null;
		m_isPaused = false;
		m_isPlaying = false;
		m_shouldLoop = false;
		m_gain = (float) 0.0; // default gain - silence
		m_configuredGain = (float) 0.0;
		m_unloadWhenFaded = false;
		m_muted = false;
		m_inputStream = null;
		m_loadThread = new LoadThread(this);
		m_loadThread.setPriority(m_loadThread.getPriority() - 2); // load thread - lower priority
	}

//  hgb - since this class is now abstract, the following mainline no longer makes sense.
//
//	public static void main(String[] args) {
//		
//		Track soundtest;
//		URL url;
//		
//		try {
//			url = new URL("http://devel0.gcrc.carleton.ca/~amoshayes/test.au");
//			System.out.println("Loading Sound...");
//			soundtest = new Track(url);
//			soundtest.setShouldLoop(true);
//			soundtest.setPlaying(true);
//			System.out.println("Sound played.");
//		} catch (MalformedURLException e) {
//			System.out.println("Error.");
//			e.printStackTrace();
//		}
//	}
	
	protected final class Debug {
		  protected static final int standardLevel = 0;
		  protected static final int mediumLevel = 1;
		  protected static final int detailLevel = 2;
		  
		  protected static final boolean ON = false;
		  protected static final int LEVEL = standardLevel;
	} 
	
	protected void DebugLog(String str) {
		if (Debug.ON) System.out.println(str);
	}

	protected void DebugLog(String str, int level) {
		if (Debug.ON && level <= Debug.LEVEL) System.out.println(str);
	}
	
	public String getOriginalUrlString() {
		return(m_origUrlString);
	}

	public void setPaused(boolean flag_) {
		DebugLog("AudioTrack::setPaused(): " + m_url + (flag_?" (PAUSED)":" (resumed)"));
		m_isPaused = flag_;
		playSourceLine();
	}
	
	synchronized public void setPlaying(boolean flag_) {
		DebugLog("AudioTrack::setPlaying(): " + m_url + (flag_?" (play)":" (STOP)"));
		m_isPlaying = flag_;
		playSourceLine();
	}
	
	synchronized public void setShouldLoop(boolean flag_) {
		m_shouldLoop = flag_;
	}
	
	/**
	 * Set the track's gain.  If the track is muted, simply store the new
	 * setting for future reference but do nothing else with it now.
	 * 
	 * @param gain_ the new gain setting  (0.0 .. 1.0).
	 */
	public void setGain(float gain_) {
		DebugLog("AudioTrack::setGain: " + m_url + " to " + gain_ + (m_muted?" (muted)":""));
		m_configuredGain = gain_;
		setGainNoDebug(gain_);
	}
	
	/**
	 * Return the track's current gain setting.
	 * @return gain setting in the range 0.0 .. 1.0.
	 */
	public float getGain() {
		return(m_gain);
	} // end getGain()
	
	/**
	 * Return the track's current configured gain setting.  Note that this may be different from the
	 * currently active gain setting if a fade is in progress.
	 * @return gain setting in the range 0.0 .. 1.0.
	 */
	public float getConfiguredGain() {
		return(m_configuredGain);
	} // end getConfiguredGain()
	
	/**
	 * Set the target configured gain.  This is only used for tracking changes to the gain setting
	 * from the track manager.
	 * @param gain_ gain setting in the range 0.0 .. 1.0.
	 */
	public void setConfiguredGain(float gain_) {
		m_configuredGain = gain_;	
	} // setConfiguredGain()
	
	private void setGainNoDebug(float gain_) {
		m_gain = gain_;
		if (false == m_muted) { // no point trying to enact new setting if muted
			changeGain();
		}		
	}
	
	/**
	 * Set the mute flag for the track and initiate the new behaviour.
	 * 
	 * When muting, subsequent gain settings will be accepted, stored but ignored until the muting
	 * condition is removed.
	 * 
	 * @param flag_ new setting for the track's muting condition.
	 */
	public void setMuting(boolean flag_) {
		m_muted = flag_;
		DebugLog("AudioTrack::setMuting: " + m_url + " muteFlag: " + flag_);
		changeGain();
	}		

	/**
	 * Return whether the track was told to unload itself after fading was complete.
	 * 
	 * @return true if the track should be unloaded once the fade is completed.
	 */
	public boolean isUnloadAfterFadeSet() {
		return(m_unloadWhenFaded);
	} // end isUnloadAfterFadeSet()
	
	/**
	 * Set the flag indicating whether the track should be unloaded after the fade operation
	 * completes according to the input flag_.
	 * @param flag_ true indicates that track should be unloaded after the current fade operation; false otherwise.
	 */
	public void setUnloadAfterFade(boolean flag_) {
		m_unloadWhenFaded = flag_;
	} // setUnloadAfterFade()
	
	/**
	 * Adjusts all attributes of the track in one call.
	 * @param playing_ If set, the track is started, if it is not already started.
	 * @param looping_ If set, the track will restart at the end.
	 * @param paused_ If set, the track is momentarily stopped.
	 * @param gain_ Volume that the track should be playing at.
	 */
	public void manageAttributes(boolean playing_, boolean looping_, boolean paused_) {
		DebugLog("AudioTrack::manageAttributes: " + (playing_ ? "play ":"stop ") + (looping_ ? "looping ":" ") +
				 (paused_ ? "paused":""));
//		if (isLoadThreadRunning()) {
//			DebugLog("AudioTrack::manageAttributes: track not yet loaded.");
//			return; // come back later - not ready for this track to be managed yet.
//		}
		if( m_isPlaying != playing_ || m_shouldLoop != looping_ || m_isPaused != paused_ ) {
			m_isPlaying = playing_;
			m_shouldLoop = looping_;
			m_isPaused = paused_;
			playSourceLine();
		}
//		if( gain_ != m_gain ) {
//			m_gain = gain_;
//			changeGain();
//		}
	}
	
	protected float computeGainIndB() {
		float dB;
		float effectiveGain;
		
		if (m_muted) {
			effectiveGain = (float) 0.0;
		} else { 
			effectiveGain = m_gain;
		}
		
		if (0 == effectiveGain) {
			return((float)-80.0);
		} else {
			dB = (float)(Math.log(effectiveGain)*20.0);
			if ((float)-80.0 > dB) {
				dB = (float)-80.0;
			}
			return(dB);
		}
	}
		
	protected void changeGain() {	
		float dB;

		if (null != m_gainControl) {
			dB = computeGainIndB();		
			m_gainControl.setValue(dB); // there may be a time lag for the gain change to take as
			                            // some data may be internally buffered. This is the case with
			                            // some mixers.
		}
	}
	
	private class ConversionProviderOrder {
		protected static final int sysIndex = 0; // the system conversion providers
		protected static final int oggIndex = 1; // bundled ogg conversion provider
		protected static final int mp3Index = 2; // bundled mp3 conversion provider
		protected static final int numberProviders = 3;
		
		private int[] tryOrder = new int[numberProviders];
		private int providersTried;
		
		public ConversionProviderOrder(URL url_) {
			String urlPath;
			
			providersTried = 0;
			for (int i=0; i<numberProviders; i++) {
				tryOrder[i] = i; // default order...
			}
			
			urlPath = url_.getPath();
			if (urlPath != null) {
				urlPath.toLowerCase();
				
				if (urlPath.endsWith(".mp3")) {
					tryOrder[mp3Index] = 0;
					tryOrder[sysIndex] = 1;
					tryOrder[oggIndex] = 2; // probably unnecessary but...
				} else if (urlPath.endsWith("ogg")) {
					tryOrder[oggIndex] = 0;
					tryOrder[sysIndex] = 1;
					tryOrder[mp3Index] = 2; // probably unnecessary but...
				} 
			} 
		} // end constructor

		/*
		 * return true if the conversion provider corresponding to index should be tried.
		 */
		public boolean checkTry(int index) {
			return(tryOrder[index] == providersTried);
		} // end checkTry()
		
		/*
		 * Update flags for for index after attempt.
		 */
		public void tryCompleted() {
			providersTried++;
		} // end tried
		
		/*
		 * Clear tried flags for whole set.
		 */
		public void clearProviderAttempts() {
			providersTried = 0;
		} // end clearTriedFlags()
		
		/*
		 * Return true if any conversion providers not tried.
		 */
		public boolean providersStillToTry() {
			return(providersTried < numberProviders);
		}
	} // end ConversionProviderOrder class
	
	/**
     * Use the standard AudioSystem infrastructure to get the input stream
     * for the input URL. If that does not work, explicitly attempt to
     * open the URL as a Vorbis audio stream or as an MP3 audio stream.  This is done on the
     * assumption that the Ogg Vorbis SPI and the MP3 SPI code code is being packaged as part of
     * the applet jar but may not be available in the target system's
     * classpath.
     *
     * @return an audio input stream for the m_url.
     */
    protected AudioInputStream getAudioInputStreamFromUrl(ConversionProviderOrder order) {
    		AudioInputStream temp_inputStream;
    				
		do {
			if (order.checkTry(ConversionProviderOrder.sysIndex)) {
				try {
					temp_inputStream = AudioSystem.getAudioInputStream(m_url);
					return(temp_inputStream);
				} catch (UnsupportedAudioFileException exIAE) { // installed libs on client system didn't include this format? Try compressed!
					// do nothing
				} catch (Exception exGeneral) {
					DebugLog("getAudioInputStreamFromUrl: Unknown error when attempting to open input stream using system providers: " + m_url);
					exGeneral.printStackTrace();
				}
				order.tryCompleted();
			}
			
			if (order.checkTry(ConversionProviderOrder.oggIndex)) {
				try {
					VorbisAudioFileReader vReader = new VorbisAudioFileReader();
					temp_inputStream = vReader.getAudioInputStream(m_url);
					return(temp_inputStream);
				} catch (UnsupportedAudioFileException exNotOgg) {
					// do nothing
				} catch (Exception exOggGeneral) {
					DebugLog("getAudioInputStreamFromUrl: Unknown error when attempting to open input stream as ogg vorbis: " + m_url);
					exOggGeneral.printStackTrace();
				}
				order.tryCompleted();
			} // end if (try ogg)
			
			if (order.checkTry(ConversionProviderOrder.mp3Index)) {
				try {
					MpegAudioFileReader mReader = new MpegAudioFileReader();
					temp_inputStream = mReader.getAudioInputStream(m_url);
					return(temp_inputStream);
				} catch (UnsupportedAudioFileException exNotMpeg) { // explicit attempt to process as Ogg Vorbis failed?  Try MP3
					// do nothing
				} catch (Exception exMpegGeneral) {
					DebugLog("getAudioInputStreamFromUrl: Unknown error when attempting to open input audio stream as mpeg: " + m_url);
					exMpegGeneral.printStackTrace();
				}
				order.tryCompleted();
			}
		} while (order.providersStillToTry());
		
		return(null);
	} // getAudioInputStreamFromUrl


    /**
     * Use the standard AudioSystem infrastructure to get an input conversion stream. If that fails
     * then explictly attempt to create an Ogg Vorbis or MP3 conversion input stream. This is done on the
     * assumption that the Ogg Vorbis and MP3 SPI code is being packaged as part of the applet jar but may
     * not be available in the target system's classpath.
     *
     * @param inStream the audio stream to be converted.
     * @return output conversion audio stream.
     */
   protected AudioInputStream getConversionAudioInputStream(AudioInputStream inStream, ConversionProviderOrder order) {
    		AudioInputStream outStream;
		int nSampleSizeInBits;
		AudioFormat format;

		// create a PCM_SIGNED format as target format for conversion
		format = inStream.getFormat();
		nSampleSizeInBits = format.getSampleSizeInBits();
		if (nSampleSizeInBits <= 0) nSampleSizeInBits = 16;
		if ((format.getEncoding() == AudioFormat.Encoding.ULAW) ||
			(format.getEncoding() == AudioFormat.Encoding.ALAW))  {
			nSampleSizeInBits = 16;
		}
		if (nSampleSizeInBits != 8) nSampleSizeInBits = 16;
		format = new AudioFormat(
				AudioFormat.Encoding.PCM_SIGNED,
				format.getSampleRate(),
				nSampleSizeInBits,
				format.getChannels(),
				format.getChannels() * (nSampleSizeInBits / 8),
				format.getSampleRate(),
				false); // ogg only works as little endian (not sure why...)
		DebugLog("getConversionAudioInputStream: requested target conversion format: " + format);
		
		do {
			if (order.checkTry(ConversionProviderOrder.sysIndex)) {
				try {
					outStream = AudioSystem.getAudioInputStream(format, inStream);
					return(outStream);
				} catch (IllegalArgumentException exIAE) { // installed decoders on client system not appropriate.  Try vorbis!
					// do nothing
				} catch (Exception exGeneral) {
					DebugLog("getConversionAudioInputStream: unknown error when converting non-PCM audio to PCM: " + m_url);
				}
				order.tryCompleted();
			}
			
			if (order.checkTry(ConversionProviderOrder.oggIndex)) {
				try {
					VorbisFormatConversionProvider vCp = new VorbisFormatConversionProvider();
					outStream = vCp.getAudioInputStream(format, inStream);
					return(outStream);
				} catch (IllegalArgumentException exVorbisIAE) { // Vorbis decoder not appropriate - try Mpeg!
					// do nothing
				} catch (Exception exVorvis) {
					DebugLog("tryBundledConversionProviders: unknown error when converting non-PCM audio using ogg vorbis converter: " + m_url);
				}
				order.tryCompleted();
			} // end if (try ogg)
			
			if (order.checkTry(ConversionProviderOrder.mp3Index)) {
				try {
					MpegFormatConversionProvider mCp = new MpegFormatConversionProvider();
					outStream = mCp.getAudioInputStream(format, inStream);
					return(outStream);
				} catch (IllegalArgumentException exMpegIAE) { // Vorbis decoder not appropriate - try Mpeg!
					// do nothing
				} catch (Exception exMpeg) {
					DebugLog("tryBundledConversionProviders: unknown error when converting non-PCM audio using mp3 converter: " + m_url);
				} 
				order.tryCompleted();
			}
		} while (order.providersStillToTry());
		
		return(null);
	} // getConversionAudioInputStream()

	/**
	 * Get an AudioInputStream for the stored URL.  Convert it to a decoding AudioInputStream if necessary.
	 * 
	 * @return the AudioInputStream, converted if necessary.
	 */
	protected AudioInputStream getAndConvertAudioInputStream() {
		AudioInputStream temp;
		ConversionProviderOrder order = new ConversionProviderOrder(m_url);

		temp = getAudioInputStreamFromUrl(order);
		if (null == temp) {
			DebugLog("getAndConvertAudioInputStream: failed to open URL " + m_url);
		    return(null); // debug information already generated
		}

		try {
			AudioFormat format = temp.getFormat();
			DebugLog("getAndConvertAudioInputStream: Input audio stream format: " + format);
			
			/**
			 * we can't yet open the device for compressed format playback (ALAW/ULAW,
			 * ogg vorbis, etc.) - convert ALAW/ULAW to PCM
			 */
			if (format.getEncoding() != AudioFormat.Encoding.PCM_SIGNED)
			{
				order.clearProviderAttempts();
				temp = getConversionAudioInputStream(temp, order);
			}
		} catch (Exception ex) { 
			DebugLog("getAndConvertAudioInputStream: Unable to load and/or convert audio source." + m_url);
			ex.printStackTrace();
			return(null);
		}
		
		return(temp);
	} // getAndConvertAudioInputStream()
	
	protected abstract void loadSound();

	protected abstract void playSourceLine();
	
	public void destroy() {
		m_isPlaying = false;

		if (null != m_loadThread) {
			try {
				m_loadThread.join();
			} catch (InterruptedException e) {
			}
		}
		
		if (null != m_currentDataLine) {
			m_currentDataLine.close();
		}
		m_currentDataLine = null;
	}
	
	public void unload() {
		destroy();
	}
	
	protected boolean isLoadThreadRunning() {
		if (null == m_loadThread) {
			return false; // doesn't appear to have ever run
		}
		
		if (Thread.State.TERMINATED == m_loadThread.getState()) {
			return false; // terminated - can't still be running
		}
		
		return true; // still running.
	} // isLoadThreadRunning()

	protected boolean hasLoadThreadCompletedNormally() {
		if (null == m_loadThread) {
			return false;  // no sign that it ran - can't have completed normally
		}
		
		if (Thread.State.TERMINATED == m_loadThread.getState()) {
			// minimal conditions for normal termination.
			return (m_currentDataLine != null && m_inputStream != null); 
		}
		
		return false; // must still be running.
	} // hasLoadThreadCompletedNormally()
	
	class LoadThread extends Thread {
		protected AudioTrack m_parentTrack;
		
		public LoadThread(AudioTrack parentTrack_) {
			m_parentTrack = parentTrack_;
			start();
		}
		public void run() {
			m_parentTrack.loadSound();
		}
	} // end of LoadThread class
	
	/*
	 * D e b u g   d i s p l a y   r o u t i n e s
	 */
	
	/**
	 * Display status of track.
	 */
	public void displayOnLog() {
		System.out.println(" AudioTrack: " + m_url + " - " + (m_isPlaying ? "play ":"stop ") +
				(m_shouldLoop ? "(looping) ":" ") +
				(m_isPaused ? "(paused) ":" ") + m_gain);
	} // displayOnLog()

} // end of AudioTrack class
