package tts;

import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

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.LineListener;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;

import marytts.util.data.audio.MonoAudioInputStream;
import marytts.util.data.audio.StereoAudioInputStream;

/**
 * A single Thread Audio Player Once used it has to be initialised again
 * 
 * @author GOXR3PLUS
 *
 */
public class AudioPlayer extends Thread {

	public static final int		MONO		= 0;
	public static final int		STEREO		= 3;
	public static final int		LEFT_ONLY	= 1;
	public static final int		RIGHT_ONLY	= 2;
	private AudioInputStream	ais;
	private LineListener		lineListener;
	private SourceDataLine		line;
	private int					outputMode;

	private Status	status			= Status.WAITING;
	private boolean	exitRequested	= false;
	private float	gain			= 1.0f;

	/**
	 * The status of the player
	 * 
	 * @author GOXR3PLUS
	 *
	 */
	public enum Status {
		/**
		 * 
		 */
		WAITING,
		/**
		* 
		*/
		PLAYING;
	}

	/**
	 * AudioPlayer which can be used if audio stream is to be set separately,
	 * using setAudio().
	 *
	 */
	public AudioPlayer() {
	}

	/**
	 * @param audioFile
	 * @throws IOException
	 * @throws UnsupportedAudioFileException
	 */
	public AudioPlayer(File audioFile) throws IOException, UnsupportedAudioFileException {
		this.ais = AudioSystem.getAudioInputStream(audioFile);
	}

	/**
	 * @param ais
	 */
	public AudioPlayer(AudioInputStream ais) {
		this.ais = ais;
	}

	/**
	 * @param audioFile
	 * @param lineListener
	 * @throws IOException
	 * @throws UnsupportedAudioFileException
	 */
	public AudioPlayer(File audioFile, LineListener lineListener) throws IOException, UnsupportedAudioFileException {
		this.ais = AudioSystem.getAudioInputStream(audioFile);
		this.lineListener = lineListener;
	}

	/**
	 * @param ais
	 * @param lineListener
	 */
	public AudioPlayer(AudioInputStream ais, LineListener lineListener) {
		this.ais = ais;
		this.lineListener = lineListener;
	}

	/**
	 * @param audioFile
	 * @param line
	 * @param lineListener
	 * @throws IOException
	 * @throws UnsupportedAudioFileException
	 */
	public AudioPlayer(File audioFile, SourceDataLine line, LineListener lineListener)
			throws IOException, UnsupportedAudioFileException {
		this.ais = AudioSystem.getAudioInputStream(audioFile);
		this.line = line;
		this.lineListener = lineListener;
	}

	/**
	 * @param ais
	 * @param line
	 * @param lineListener
	 */
	public AudioPlayer(AudioInputStream ais, SourceDataLine line, LineListener lineListener) {
		this.ais = ais;
		this.line = line;
		this.lineListener = lineListener;
	}

	/**
	 * 
	 * @param audioFile
	 *            audiofile
	 * @param line
	 *            line
	 * @param lineListener
	 *            lineListener
	 * @param outputMode
	 *            if MONO, force output to be mono; if STEREO, force output to
	 *            be STEREO; if LEFT_ONLY, play a mono signal over the left
	 *            channel of a stereo output, or mute the right channel of a
	 *            stereo signal; if RIGHT_ONLY, do the same with the right
	 *            output channel.
	 * @throws IOException
	 *             IOException
	 * @throws UnsupportedAudioFileException
	 *             UnsupportedAudioFileException
	 */
	public AudioPlayer(File audioFile, SourceDataLine line, LineListener lineListener, int outputMode)
			throws IOException, UnsupportedAudioFileException {
		this.ais = AudioSystem.getAudioInputStream(audioFile);
		this.line = line;
		this.lineListener = lineListener;
		this.outputMode = outputMode;
	}

	/**
	 * 
	 * @param ais
	 *            ais
	 * @param line
	 *            line
	 * @param lineListener
	 *            lineListener
	 * @param outputMode
	 *            if MONO, force output to be mono; if STEREO, force output to
	 *            be STEREO; if LEFT_ONLY, play a mono signal over the left
	 *            channel of a stereo output, or mute the right channel of a
	 *            stereo signal; if RIGHT_ONLY, do the same with the right
	 *            output channel.
	 */
	public AudioPlayer(AudioInputStream ais, SourceDataLine line, LineListener lineListener, int outputMode) {
		this.ais = ais;
		this.line = line;
		this.lineListener = lineListener;
		this.outputMode = outputMode;
	}

	/**
	 * @param audio
	 */
	public void setAudio(AudioInputStream audio) {
		if (status == Status.PLAYING) {
			throw new IllegalStateException("Cannot set audio while playing");
		}
		this.ais = audio;
	}

	/**
	 * Cancel the AudioPlayer which will cause the Thread to exit
	 */
	public void cancel() {
		if (line != null) {
			line.stop();
		}
		exitRequested = true;
	}

	/**
	 * @return The SourceDataLine
	 */
	public SourceDataLine getLine() {
		return line;
	}

	/**
	 * Returns the GainValue
	 */
	public float getGainValue() {
		return gain;
	}

	/**
	 * Sets Gain value. Line should be opened before calling this method. Linear
	 * scale 0.0 <--> 1.0 Threshold Coef. : 1/2 to avoid saturation.
	 * 
	 * @param fGain
	 */
	public void setGain(float fGain) {

		// if (line != null)
		// System.out.println(((FloatControl)
		// line.getControl(FloatControl.Type.MASTER_GAIN)).getValue())

		// Set the value
		gain = fGain;

		// Better type
		if (line != null && line.isControlSupported(FloatControl.Type.MASTER_GAIN))
			((FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN))
					.setValue((float) (20 * Math.log10(fGain <= 0.0 ? 0.0000 : fGain)));
		// OR (Math.log(fGain == 0.0 ? 0.0000 : fGain) / Math.log(10.0))

		// if (line != null)
		// System.out.println(((FloatControl)
		// line.getControl(FloatControl.Type.MASTER_GAIN)).getValue())
	}

	@Override
	public void run() {

		status = Status.PLAYING;
		AudioFormat audioFormat = ais.getFormat();
		if (audioFormat.getChannels() == 1) {
			if (outputMode != 0) {
				ais = new StereoAudioInputStream(ais, outputMode);
				audioFormat = ais.getFormat();
			}
		} else {
			assert audioFormat.getChannels() == 2 : "Unexpected number of channels: " + audioFormat.getChannels();
			if (outputMode == 0) {
				ais = new MonoAudioInputStream(ais);
			} else if (outputMode == 1 || outputMode == 2) {
				ais = new StereoAudioInputStream(ais, outputMode);
			} else {
				assert outputMode == 3 : "Unexpected output mode: " + outputMode;
			}
		}

		DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);

		try {
			if (line == null) {
				boolean bIsSupportedDirectly = AudioSystem.isLineSupported(info);
				if (!bIsSupportedDirectly) {
					AudioFormat sourceFormat = audioFormat;
					AudioFormat targetFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
							sourceFormat.getSampleRate(), sourceFormat.getSampleSizeInBits(),
							sourceFormat.getChannels(),
							sourceFormat.getChannels() * (sourceFormat.getSampleSizeInBits() / 8),
							sourceFormat.getSampleRate(), sourceFormat.isBigEndian());

					ais = AudioSystem.getAudioInputStream(targetFormat, ais);
					audioFormat = ais.getFormat();
				}
				info = new DataLine.Info(SourceDataLine.class, audioFormat);
				line = (SourceDataLine) AudioSystem.getLine(info);
			}
			if (lineListener != null) {
				line.addLineListener(lineListener);
			}
			line.open(audioFormat);
		} catch (Exception ex) {
			Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex);
			return;
		}

		line.start();
		setGain(getGainValue());

		int nRead = 0;
		byte[] abData = new byte[65532];
		while ((nRead != -1) && (!exitRequested)) {
			try {
				nRead = ais.read(abData, 0, abData.length);
			} catch (IOException ex) {
				Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex);
			}
			if (nRead >= 0) {
				line.write(abData, 0, nRead);
			}
		}
		if (!exitRequested) {
			line.drain();
		}
		line.close();
	}

}