package gnu.javax.sound.sampled.wce;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioPermission;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;

/**
 * 音声出力ライン。
 */
public class WCESourceDataLine extends WCEDataLine
										implements SourceDataLine {
	/**
	 * ネイティブデータ
	 */
	private int nativeData;
	
	/**
	 * 出力ラインのインスタンスを作成する。
	 *
	 * @param	info	このLineの情報
	 */
	public WCESourceDataLine(DataLine.Info info) {
		super(info);
	}
	
	/**
	 * このラインをデフォルト設定でオープンする
	 *
	 */
	public void open()
			throws LineUnavailableException,
				   IllegalArgumentException,
				   SecurityException {
		open(getFormat());
	}
	
	/**
	 * このラインをオープンする
	 *
	 * @param	format	フォーマット
	 * @param	bufferSize	バッファサイズ
	 */
	public synchronized void open(AudioFormat format, int bufferSize)
								throws LineUnavailableException,
									   IllegalArgumentException,
									   IllegalStateException,
									   SecurityException {
		if (isOpen()) {
			throw new IllegalStateException("Already open");
		}
		
		// セキュリティチェック
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			// "play" 権限をチェックする
			sm.checkPermission(new AudioPermission("play"));
		}
		
		// フォーマットをチェックする
		checkFormat(format);
		
		// オープンする
		super.open();
		this.nativeData
			=  openNative(bufferSize,
						  format.getSampleRate(),
						  format.getSampleSizeInBits(),
						  format.getChannels(),
						  format.getFrameSize(),
						  format.getFrameRate(),
						  format.isBigEndian());
		setFormat(format);
	}
	
	/**
	 * このラインをオープンする
	 *
	 * @param	format	フォーマット
	 */
	public void open(AudioFormat format)
								throws LineUnavailableException,
									   IllegalArgumentException,
									   IllegalStateException,
									   SecurityException {
		open(format, 0);
	}
	
	/**
	 * Windows CEの音声出力デバイスをオープンする。
	 *
	 * @param	bufferSize	バッファサイズ
	 * @param	sampleRate	サンプルレート
	 * @param	sampleSizeInBits	ビット単位で表したサンプルデータサイズ。
	 * @param	channels	チャンネル数。
	 * @param	frameSize	1フレームのデータバイト数
	 * @param	frameRate	1秒あたりの再生フレーム数
	 * @param	bigEndian	データがビッグエンディアンの場合true。
	 *
	 * @return	ネイティブデータ。（構造体のポインタ）
	 *
	 * @throws LineUnavailableException
	 *						音声出力デバイスがオープンできない場合。
	 */
	private native int openNative(int bufferSize,
								  float sampleRate,
								  int sampleSizeInBits,
								  int channels,
								  int frameSize,
								  float frameRate,
								  boolean bigEndian)
								throws LineUnavailableException,
									   IllegalArgumentException,
									   IllegalStateException,
									   SecurityException;
	
	/**
	 * オーディをデータをラインに書き込む
	 */
	public int write(byte[] b, int off, int len)
							throws IllegalArgumentException,
								   ArrayIndexOutOfBoundsException {
		if (! isOpen()) {
			return 0;
		}
		return writeNative(this.nativeData,
						   b,
						   off,
						   len);
	}
	
	/**
	 * オーディオ出力デバイスに、波形データを書き込む
	 *
	 * @param	nativeData	ネイティブデータ
	 * @param	b			バッファ
	 * @param	off
	 * @param	len
	 */
	private native int writeNative(int nativeData,
								   byte[] b,
								   int off,
								   int len)
							throws IllegalArgumentException,
								   ArrayIndexOutOfBoundsException;

	/**
	 * ラインをクローズする
	 */
	public void close() {
		if (isOpen()) {
			stop();
			flush();
			nativeClose(this.nativeData);
			super.close();
		}
	}
	
	/**
	 * Windows CEの音声出力デバイスをクローズする
	 */
	private native void nativeClose(int nativeData);
	
	public void drain() {
		if (! isOpen()) {
			return;
		}
		drainNative(this.nativeData);
	}
	
	/**
	 * バッファ内のデータが空になるまでブロックする
	 */
	private native void drainNative(int nativeData);
	
	public void flush() {
		if (! isOpen()) {
			return;
		}
		flushNative(this.nativeData);
	}
	
	/**
	 * 出力バッファ内のデータを破棄する
	 */
	private native void flushNative(int nativeData);

	public boolean isActive() {
		return isNativeActive(this.nativeData);
	}
	
	/**
	 * Windows CEデバイス側で音声出力中かを返す。
	 */
	private native boolean isNativeActive(int nativeData);

	public int getBufferSize() {
		if (! isOpen()) {
			return 0;
		}
		return getNativeBufferSize(this.nativeData);
	}
	
	/**
	 * ネイティブバッファサイズを返す
	 */
	private native int getNativeBufferSize(int nativeData);
	
	public int available() {
		if (! isOpen()) {
			return 0;
		}
		return availableNative(this.nativeData);
	}
	
	/**
	 * ネイティブライブラリ側でブロックせずに書き込めるバイト数を
	 * 返す。
	 */
	private native int availableNative(int nativeData);
	
	public int getFramePosition() {
		return (int) getLongFramePosition();
	}
	
	public long getLongFramePosition() {
		if (! isOpen()) {
			return 0L;
		}
		// ToDo: 実装する
		return AudioSystem.NOT_SPECIFIED;
	}
	
	public long getMicrosecondPosition() {
		if (! isOpen()) {
			return 0L;
		}
		// ToDo: 実装する
		return AudioSystem.NOT_SPECIFIED;
	}
	
	public float getLevel() {
		// ToDo: 実装する
		return 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 start() {
		if (! isOpen()) {
			return;
		}
		
		super.start();
		startNative(this.nativeData);
	}
	
	private native void startNative(int nativeData);
	
	public void stop() {
		if (! isOpen()) {
			return;
		}
		stopNative(this.nativeData);
		super.stop();
	}

	private native void stopNative(int nativeData);

	
	/**
	 * フォーマットをチェックする
	 */
	protected static void checkFormat(AudioFormat format) throws LineUnavailableException {
		// フォーマットをチェックする
		// サポートしているのは以下の２つ
		// o  8ビット符号なしPCM
		// o 16ビット符号付きPCM
		boolean validFormat = false;
		int sampleSizeInBits = format.getSampleSizeInBits();
		AudioFormat.Encoding encoding = format.getEncoding();
		switch (sampleSizeInBits) {
		case 8:
			// ８ビットの場合は符号なし
			if (AudioFormat.Encoding.PCM_UNSIGNED.equals(encoding)) {
				validFormat = true;
			}
			break;
			
		case 16:
			// 16ビットの場合は符号付
			if (AudioFormat.Encoding.PCM_SIGNED.equals(encoding)) {
				validFormat = true;
			}
			break;
		}
		if (format.getChannels() > 0) {
			// チャネル数をチェック
			validFormat = true;
		}
		
		if (! validFormat) {
			throw new LineUnavailableException("Unsupported format:" + format);
		}
	}
}
