/* WCEClip.java
   Copyright (C) 2006 Mysaifu.com

This file is part of Mysaifu JVM.

Mysaifu JVM is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; the version 2.

Mysaifu JVM 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
General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA.

Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.

As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module.  An independent module is a module which is not derived from
or based on this library.  If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so.  If you do not wish to do so, delete this
exception statement from your version. */


package gnu.javax.sound.sampled.wce;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioPermission;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;

public class WCEClip extends WCEDataLine implements Clip {
	/**
	 * サウンドデータ
	 */
	private byte[] soundData;

	/**
	 * 音声出力先
	 */
	private final WCESourceDataLine sourceLine;
	
	/**
	 * 再生用スレッド
	 */
	private Thread playbackThread;
	
	/**
	 * 開始位置
	 */
	private int startFrame;
	
	/**
	 * ループ開始位置
	 */
	private int loopStart;
	
	/**
	 * ループ終了位置
	 */
	private int loopEnd;
	
	/**
	 * ループカウント
	 */
	private int loopCount;
	
	/**
	 * 再生が完了した
	 */
	private volatile boolean playbackCompleted;
	
	public WCEClip(DataLine.Info info) {
		super(info);
		this.sourceLine = new WCESourceDataLine(info);
		
		// START/STOPイベントをリダイレクトするために
		// リスナを設定する
		this.sourceLine.addLineListener(new LineListener() {
			public void update(LineEvent e) {
				if (e.getType() == LineEvent.Type.START) {
					notifyLineEvent(new LineEvent(WCEClip.this,
										 LineEvent.Type.START,
										 AudioSystem.NOT_SPECIFIED));
				} else if (e.getType() == LineEvent.Type.STOP) {
					notifyLineEvent(new LineEvent(WCEClip.this,
										 LineEvent.Type.STOP,
										 AudioSystem.NOT_SPECIFIED));
				}
			}
		});
	}
	
	/**
	 * 0バイトのクリップをオープンする
	 */
	public void open() throws LineUnavailableException {
		open(getDefaultFormat(), new byte[0], 0, 0);
	}
	
	/**
	 * 指定されたバイト配列で表される音声データをオープンする
	 */
	public void open(AudioFormat format,
	                 byte[] data,
	                 int offset,
	                 int bufferSize)
	                 	throws LineUnavailableException {

		// バッファにデータをコピーする
		this.soundData = new byte[bufferSize];
		System.arraycopy(data, offset, this.soundData, 0, bufferSize);

		open(format);
	}
	
	/**
	 * 指定されたストリームからデータを読み込み、このClipをオープンする
	 */
	public void open(AudioInputStream stream)
						throws LineUnavailableException, IOException {
		// バッファにデータを読み込む
		byte[] buff = new byte[1024];
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		int len;
		while ((len = stream.read(buff)) != -1) {
			baos.write(buff, 0, len);
		}
		baos.close();
		this.soundData = baos.toByteArray();
		
		open(stream.getFormat());
	}
	
	private void open(AudioFormat format) throws LineUnavailableException {
		// アクセスチェック
		checkAccess();
		
		// フォーマットをチェック
		WCESourceDataLine.checkFormat(format);

		// オープン
		super.open();

		// フォーマットを設定
		setFormat(format);

		// 位置を初期化
		setFramePosition(0);
		setLoopPoints(0, -1);
		
		// ループ回数を初期化
		loop(0);

		// 出力デバイスをオープン
		this.sourceLine.open(format);


		// イベントを通知する
		notifyLineEvent(new LineEvent(this,
							 LineEvent.Type.OPEN,
							 AudioSystem.NOT_SPECIFIED));
	}
	
	/**
	 * このクリップを閉じる
	 */
	public void close() {
		if (! isOpen()) {
			return;
		}
		stop();
		super.close();
		
		// ブロックしているスレッドに通知する
		synchronized (this) {
			// 再生完了
			this.playbackCompleted = true;
			notifyAll();
		}

		if (this.playbackThread != null) {
			// スレッドが終了するのを待つ
			try {
				this.playbackThread.join();
			} catch (InterruptedException ie) {
				ie.printStackTrace();
			}
		}
		this.playbackThread = null;
		
		// サウンドデータをクリアする
		this.soundData = null;
		
		// 音声出力先をクローズする
		this.sourceLine.close();
		
		// イベントを通知する
		notifyLineEvent(new LineEvent(this,
							 LineEvent.Type.CLOSE,
							 AudioSystem.NOT_SPECIFIED));
	}
	
	public Control[] getControls() {
		// ToDo: 実装する
		return new Control[0];
	}
	
	public boolean isControlSupported(Control.Type control) {
		// ToDo: 実装する
		return false;
	}
	
	public Control getControl(Control.Type control) {
		// ToDo: 実装する
		throw new IllegalArgumentException("Not supported:" + control);
	}
	
	public void drain() {
		if (! isOpen()) {
			return;
		}
		synchronized (this) {
			while (! this.playbackCompleted) {
				try {
					wait();
				} catch (InterruptedException ie) {
					ie.printStackTrace();
				}
			}
		}
		this.sourceLine.drain();
	}
	
	public void flush() {
		if (! isOpen()) {
			return;
		}
		this.sourceLine.flush();
	}
	
	public synchronized void start() {
		if (isRunning()) {
			return;
		}
		super.start();
		this.sourceLine.start();
		
		// 再生スレッドを開始する
		if (this.playbackThread == null) {
			this.playbackThread = new Thread() {
				public void run() {
					playbackLoop();
				}
			};
			this.playbackThread.start();
		}
		
		// 再生完了フラグをクリアする
		this.playbackCompleted = false;
		
		// startしたことを再生スレッドに通知する
		notifyAll();
	}
	
	/**
	 * このClipを停止する
	 */
	public synchronized void stop() {
		if (! isRunning()) {
			return;
		}
		super.stop();
		this.sourceLine.stop();
		
		if (this.playbackThread != null) {
			notifyAll();
		}
	}
	
	public boolean isActive() {
		return this.sourceLine.isActive();
	}
	
	public int getBufferSize() {
		return this.sourceLine.getBufferSize();
	}
	
	public int available() {
		// 常に0を返す
		return 0;
	}
	
	public int getFramePosition() {
		// ToDo: 実装する
		return AudioSystem.NOT_SPECIFIED;
	}
	
	public long getLongFramePosition() {
		// ToDo: 実装する
		return AudioSystem.NOT_SPECIFIED;
	}

	public long getMicrosecondPosition() {
		// ToDo: 実装する
		return AudioSystem.NOT_SPECIFIED;
	}
	
	public float getLevel() {
		// ToDo: 実装する
		return AudioSystem.NOT_SPECIFIED;
	}
	
	/**
	 * メディアのフレーム長を返す
	 */
	public int getFrameLength() {
		if (! isOpen()) {
			return 0;
		}
		return this.soundData.length / getFormat().getFrameSize();
	}
	
	/**
	 * メディアの長さをマイクロ秒単位で返す。
	 */
	public long getMicrosecondLength() {
		if (! isOpen()) {
			return 0L;
		}
		return ((long) getFrameLength() * 1000000L) / (long) getFormat().getFrameRate();
	}
	
	/**
	 * 次回再生開始位置を設定する。
	 */
	public void setFramePosition(int frames) {
		if (frames > getFrameLength()) {
			throw new IllegalArgumentException("frames=" + frames);
		}
		this.startFrame = frames;
	}
	
	/**
	 * 次回再生開始位置をマイクロ秒単位で設定する。
	 */
	public void setMicrosecondPosition(long microseconds) {
		if (getMicrosecondLength() < microseconds) {
			throw new IllegalArgumentException("microseconds=" + microseconds);
		}
		
		this.startFrame =  (int) (((long) getFormat().getFrameRate() * microseconds) / 1000000L);
	}
	
	/**
	 * ループの開始／終了位置を設定する
	 */
	public void setLoopPoints(int start, int end) {
		if (! isOpen()) {
			return;
		}
		
		if ((start < 0)
				|| (end != -1 && end >= getFrameLength())
				|| (end != -1 && (start > end))) {
			throw new IllegalArgumentException("start=" + start + " end=" + end);
		}
		
		synchronized (this) {
			this.loopStart = start;
			this.loopEnd = (end == -1) ? (getFrameLength() - 1) : end;
		}
	}
	
	/**
	 * ループ再生を開始する
	 */
	public synchronized void loop(int count) {
		this.loopCount = count;
	}
	
	/**
	 * アクセスチェックを行う
	 */
	private void checkAccess() throws SecurityException {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			// "play" 権限をチェックする
			sm.checkPermission(new AudioPermission("play"));
		}
	}
	
	/**
	 * 再生ループ
	 */
	private void playbackLoop() {
		this.playbackCompleted = false;
		while (isOpen()) {
			synchronized (this) {
				while (isOpen() && (! isRunning() || this.playbackCompleted)) {
					// stopされている間または再生が完了している場合は
					// 何らかのイベントが発生するまで待機する
					try {
						wait();
					} catch (InterruptedException ie) {
						ie.printStackTrace();
						break;
					}
				}
				if (! isOpen()) {
					// ラインがクローズされた
					break;
				}
			}

			// 一度にwrite()する長さを計算する
			final int writeUnitLength = (int) getFormat().getFrameRate();
			final int frameSize = getFormat().getFrameSize();
			
			// 開始位置と終了位置を計算する
			int nextFrame = this.startFrame;
			int endFrame = (this.loopCount != 0) ? this.loopEnd : getFrameLength() - 1;
			

			// 再生ループ開始
			while (isRunning() && (nextFrame < endFrame)) {
				// 書き込むデータ長を計算する
				int framesWrite = ((nextFrame + writeUnitLength) > endFrame)
							? endFrame - nextFrame
							: writeUnitLength;
				int offset = nextFrame * frameSize;
				int bytesWrite = framesWrite * frameSize;

				// 音声出力処理
				sourceLine.write(this.soundData,
						 		 offset,
								 bytesWrite);
								 
				// 次のフレーム位置を計算する
				synchronized (this) {
					endFrame = (this.loopCount != 0) ? loopEnd : (getFrameLength() - 1);
					if ((this.loopCount != 0) && (nextFrame + framesWrite) >= endFrame) {
						// ループする場合は、loopStartに戻る
						nextFrame = this.loopStart;
						if (this.loopCount != Clip.LOOP_CONTINUOUSLY) {
							// ループカウントを減算する
							this.loopCount--;
						}
					} else {
						// ループ位置に到達していない場合
						nextFrame += framesWrite;
					}
				}
			}
			synchronized (this) {
				// 再生が完了したことを通知する
				this.playbackCompleted = true;
				notifyAll();
			}
		}
		
	}
}
