package jp.sourceforge.nicoro;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;

import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;
//import java.util.Arrays;

import static jp.sourceforge.nicoro.Log.LOG_TAG;

/**
 * 音声のストリーム再生クラス
 */
public class StreamAudioPlayer implements AudioTrack.OnPlaybackPositionUpdateListener, StreamAudioPlayerInterface {
	private static final boolean DEBUG_LOGV = Release.IS_DEBUG & false;
	private static final boolean DEBUG_LOGD = Release.IS_DEBUG & true;

	/**
	 *
	 * FFmpegの都合上、AVCODEC_MAX_AUDIO_FRAME_SIZE以上でなければならない
	 */
	private static final int AUDIO_BUFFER_BYTE_SIZE = 192000 + 0;

//    private static final int DUMMY_DATA_SIZE = 256 * 1024;

	private static final int PLAY_ALWAYS_INTERVAL_MS = 500;

	private AudioTrackCustom mAudioTrack;
    private int mAudioSampleByteSize;
	private short[] mAudioBufferSecond;
	private int mAudioBufferSecondOffset;

	private SoftReference<byte[]> mRefDummyBuffer =
	    new SoftReference<byte[]>(null);

	private int mSampleRate;

	/**
	 * 二次バッファに書き込み済み音声データの合計サイズ（バイト単位）
	 */
	private long mWrittenBufferByteSize;
	/**
	 * デバイスに書き込み済み音声データの合計サイズ（バイト単位）<BR>
	 * TODO 再生位置情報としては使えない（シークで破綻するため）
	 */
	private long mWrittenDeviceByteSize;

	private long mLastWrittenDeviceByteSizeAtSeek;

//	private int mLastPlaybackHeadPosition;

	private int mDummyDataByteSizeAtStart;
    private long mDummyDataByteSizeAtSeek;

//    private AudioTrackCustom mAudioTrackDummyAtStart;
//    private int mWrittenDummyAtStartSize;

	private volatile boolean mIsFinish;

	private boolean mReservePause;

	private int mSeekLastState = 0;

	@SuppressWarnings("unused")
    private int mBufferSize;

	private volatile boolean mIsInSeek;
	@SuppressWarnings("unused")
    private long mSeekLastDummyStartTime;

	private LooperThread mLooperThread;

	private boolean mPlayAlways;

	private int mOffsetTimeMs;

	private static class AudioTrackCustom extends AudioTrack {
		public AudioTrackCustom(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode) {
			super(streamType, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, mode);
		}

		public int getNativeFrameCountPublic() {
			int count = getNativeFrameCount();
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf().append("getNativeFrameCount()=")
						.append(count).toString());
			}
			return count;
		}
	}

    public StreamAudioPlayer(Context context) {
        SharedPreferences sharedPreferences = Util.getDefaultSharedPreferencesMultiProcess(context);
        mPlayAlways = sharedPreferences.getBoolean(context.getResources().getString(
                R.string.pref_key_ffmpeg_audio_track_always), true);

//        mWrittenDummyAtStartSize = 0;
//        int dummySize = AudioTrack.getMinBufferSize(44100,
//                AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_8BIT);
//        mAudioTrackDummyAtStart = new AudioTrackCustom(AudioManager.STREAM_MUSIC,
//                44100, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_8BIT,
//                dummySize,
//                AudioTrack.MODE_STREAM);
////                AudioTrack.MODE_STATIC);
//        mAudioTrackDummyAtStart.setPlaybackPositionUpdateListener(
//                new AudioTrack.OnPlaybackPositionUpdateListener() {
//                    @Override
//                    public void onPeriodicNotification(AudioTrack track) {
//                    }
//                    @Override
//                    public void onMarkerReached(AudioTrack track) {
//                        int dummyWriteSize = 44100 / 10;
//                        byte[] dummy = getDummyBuffer(dummyWriteSize);
//                        int written = track.write(dummy, 0, dummyWriteSize);
//                        if (written >= 0) {
//                            mWrittenDummyAtStartSize += written;
//                            int result = track.setNotificationMarkerPosition(
//                                    mWrittenDummyAtStartSize);
//                            assert result == AudioTrack.SUCCESS;
//                        } else {
//                            assert false : written;
//                        }
//                        if (DEBUG_LOGD) {
//                            int marker = track.getNotificationMarkerPosition();
//                            Log.d(LOG_TAG, Log.buf().append("mAudioTrackDummyAtStart.onMarkerReached: marker=")
//                                    .append(marker).append(" written=").append(mWrittenDummyAtStartSize).toString());
//                        }
//                    }
//                });
////        int dummyWriteSize = 44100 / 10;
////        int dummyWriteSize = 44100 * 30;
//        int dummyWriteSize = 44100 * 1;
//        byte[] dummy = getDummyBuffer(dummyWriteSize);
//        while (dummyWriteSize > 0) {
//            int written = mAudioTrackDummyAtStart.write(dummy, 0, dummyWriteSize);
//            if (written > 0) {
//                if (DEBUG_LOGD) {
//                    Log.d(LOG_TAG, Log.buf().append("mAudioTrackDummyAtStart start: written=")
//                            .append(mWrittenDummyAtStartSize).toString());
//                }
//                mAudioTrackDummyAtStart.flush();
//                mWrittenDummyAtStartSize += written;
//                dummyWriteSize -= written;
//            } else if (written == 0) {
//                break;
//            } else {
//                assert false : written;
//                break;
//            }
//        }
//        int result = mAudioTrackDummyAtStart.setNotificationMarkerPosition(
//                mWrittenDummyAtStartSize);
//        assert result == AudioTrack.SUCCESS;
//        mAudioTrackDummyAtStart.play();
    }

    @Override
    public boolean isNull() {
        return false;
    }

    /**
     * 再生の前準備
     */
    @Override
    public void prepareStart() {
    	if (mAudioBufferSecond == null) {
    		mAudioBufferSecond = new short[getAudioBufferByteSize()/2*2];
    	}
    	mAudioBufferSecondOffset = 0;
    	mIsFinish = false;
    	mReservePause = false;
    }
    /**
     * 終了させる
     */
    @Override
    public void finish() {
    	mIsFinish = true;
    	releaseAll();
    }

    /**
     * {@link android.media.AudioTrack#release()}のみ行う
     */
    @Override
    public void release() {
		AudioTrack audioTrack = mAudioTrack;
		if (audioTrack != null) {
			audioTrack.release();
		}
//		audioTrack = mAudioTrackDummyAtStart;
//        if (audioTrack != null) {
//            audioTrack.release();
//            mAudioTrackDummyAtStart = null;
//        }
    }
    /**
     * {@link #release()}プラスAudioTrackやバッファの開放を行う
     */
    @SuppressLint("NewApi")
    @Override
    public void releaseAll() {
        release();
        mAudioTrack = null;
        mAudioBufferSecond = null;
        if (mLooperThread != null) {
            mLooperThread.quit();
            mLooperThread = null;
        }
    }

    /**
     * FFmpegによって開始<br>
     * TODO 動画または音声のどちらかがない動画への対応
     * @param sample_rate
     * @param channels
     * @param sample_fmt
     */
    @Override
    public void startByFFmpeg(int sample_rate, int channels, int sample_fmt) {
    	startCommon();
    	int sampleRateInHz = sample_rate;
    	mSampleRate = sampleRateInHz;
    	mAudioSampleByteSize = 1;
    	int channelConfig;
    	switch (channels) {
    	case 1:
    		channelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;
    		mAudioSampleByteSize *= 1;
    		break;
    	case 2:
			channelConfig = AudioFormat.CHANNEL_CONFIGURATION_STEREO;
			mAudioSampleByteSize *= 2;
			break;
		default:
		    // TODO このパラメータ渡すと例外で落ちる
			channelConfig = AudioFormat.CHANNEL_CONFIGURATION_INVALID;
    		break;
    	}
    	int audioFormat;
    	switch (sample_fmt) {
    	case 0:		// SAMPLE_FMT_U8
    		audioFormat = AudioFormat.ENCODING_PCM_8BIT;
    		mAudioSampleByteSize *= 1;
    		break;
    	case 1:		// SAMPLE_FMT_S16
    		audioFormat = AudioFormat.ENCODING_PCM_16BIT;
    		mAudioSampleByteSize *= 2;
    		break;
    	default:
            // TODO このパラメータ渡すと例外で落ちる
    		audioFormat = AudioFormat.ENCODING_INVALID;
    		break;
    	}

    	release();
    	int bufferSize = AUDIO_BUFFER_BYTE_SIZE;
        mBufferSize = bufferSize;
        mAudioTrack = createAudioTrackCustom(
        		sampleRateInHz,
        		channelConfig,
        		audioFormat,
        		bufferSize);
        mAudioTrack.play();

        // TODO インストール直後の最初の１回目はどうやってもAudioTrackがいったん待機してしまう？
        // ダミーデータ書き込み
        int dummyBufferSize = bufferSize;
        writeDummyDataAtStart(dummyBufferSize);
    }

    /**
     * SWF再生によって開始
     * @param sampleRateInHz
     * @param channels
     */
    @Override
    public void startBySwf(int sampleRateInHz, int channels) {
    	startCommon();
    	mAudioSampleByteSize = 2;
    	mSampleRate = sampleRateInHz;

    	int channelConfig;
    	switch (channels) {
    	case 1:
    		channelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;
    		mAudioSampleByteSize *= 1;
    		break;
    	case 2:
			channelConfig = AudioFormat.CHANNEL_CONFIGURATION_STEREO;
			mAudioSampleByteSize *= 2;
			break;
		default:
			channelConfig = AudioFormat.CHANNEL_CONFIGURATION_INVALID;
    		break;
    	}
    	int audioFormat = AudioFormat.ENCODING_PCM_16BIT;

    	release();
//    	int bufferSize = AUDIO_BUFFER_BYTE_SIZE;
    	int bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
    	mBufferSize = bufferSize;
        mAudioTrack = createAudioTrackCustom(
        		sampleRateInHz,
        		channelConfig,
        		audioFormat,
        		bufferSize);
        mAudioTrack.play();

        // TODO ダミーデータ書き込み：どうも再生時間の調整の効果無し？
//        int dummyBufferSize = bufferSize;
        int dummyBufferSize = mAudioTrack.getNativeFrameCountPublic() * mAudioSampleByteSize;
//        int dummyBufferSize = DUMMY_DATA_SIZE;
//        int dummyBufferSize = 5 * sampleRateInHz * mAudioSampleByteSize;
//        int dummyBufferSize = 1 * sampleRateInHz * mAudioSampleByteSize;
//        int dummyBufferSize = AUDIO_BUFFER_BYTE_SIZE;
        writeDummyDataAtStart(dummyBufferSize);
    }

    private void startCommon() {
    	mWrittenBufferByteSize = 0L;
    	mWrittenDeviceByteSize = 0L;
    	mDummyDataByteSizeAtStart = 0;
    	mDummyDataByteSizeAtSeek = 0L;
    	mOffsetTimeMs = 0;
    	mLastWrittenDeviceByteSizeAtSeek = 0;
    }

    private void writeDummyDataAtStart(int size) {
//        mDummyDataByteSizeAtStart = size;
        final AudioTrackCustom audioTrack = mAudioTrack;
        assert audioTrack != null;
        int written = writeDummyDataCommon(audioTrack, size);
        if (written >= 0) {
            mDummyDataByteSizeAtStart = written;
            int result = audioTrack.setNotificationMarkerPosition(getWrittenFrameAll());
            assert result == AudioTrack.SUCCESS;
        }
        audioTrack.flush();
    }

    @Override
    public int writeDummyDataAtSeek() {
        final AudioTrackCustom audioTrack = mAudioTrack;
        if (audioTrack == null) {
            return AudioTrack.ERROR;
        }
        final int size = AUDIO_BUFFER_BYTE_SIZE;
        int written = writeDummyDataCommon(audioTrack, size);
        if (written >= 0) {
            mDummyDataByteSizeAtSeek += written;
            int result = audioTrack.setNotificationMarkerPosition(getWrittenFrameAll());
            assert result == AudioTrack.SUCCESS;
        }
        audioTrack.flush();
        return written;
    }

    /**
     * AudioTrackを活動中にさせるため、ダミーデータを書き込み続ける
     */
    @Override
    public void writeDummyDataAtSeekForAlive() {
        final AudioTrackCustom audioTrack = mAudioTrack;
        if (audioTrack == null) {
            return;
        }

        if (getRemainDataSizeOnDeviceMs() < 33) {
            int dummyByte = (int) (33 * mSampleRate / 1000L * mAudioSampleByteSize);
            int written = writeDummyDataCommon(audioTrack, dummyByte);
            if (written >= 0) {
                mDummyDataByteSizeAtSeek += written;
                int result = audioTrack.setNotificationMarkerPosition(getWrittenFrameAll());
                assert result == AudioTrack.SUCCESS;
            }
        }
    }

    private int writeDummyDataCommon(AudioTrackCustom audioTrack, int size) {
        byte[] dummy = getDummyBuffer(size);
        int written = audioTrack.write(dummy, 0, size);
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("writeDummyDataCommon() size=")
                    .append(size).append(" written=").append(written).toString());
        }
        return written;
    }

    /**
     * 冒頭に挿入したダミーデータの再生中か判定
     * @return
     */
    @Override
    public boolean isInDummyDataAtStart() {
        final AudioTrackCustom audioTrack = mAudioTrack;
		if (audioTrack != null) {
//			final int dummyFrame = DUMMY_DATA_SIZE / mAudioSampleByteSize;
			final int dummyFrame = getWrittenFrameDummyAtStart();;
			return (audioTrack.getPlaybackHeadPosition() < dummyFrame);
		}
		return false;
    }

    @Override
    public boolean isInDummyDataAtSeek() {
        final AudioTrackCustom audioTrack = mAudioTrack;
        if (audioTrack != null) {
            final int dummyFrame = getWrittenFrameAll();
            return (audioTrack.getPlaybackHeadPosition() < dummyFrame);
        }
        return false;
    }

    /**
     * 再生可能状態か判定
     * @return
     */
    @Override
    public boolean canPlay() {
    	return mAudioTrack != null;
    }

	/**
	 * 一時停止
	 */
	@Override
    public void pause() {
        final AudioTrackCustom audioTrack = mAudioTrack;
		if (audioTrack != null) {
//			mLastPlaybackHeadPosition = audioTrack.getPlaybackHeadPosition();
		    if (mIsInSeek) {
		        mReservePause = true;
		    } else {
		        // ただのpauseでも
		        // I/AudioHardwareQSD: AudioHardware pcm playback is going to standby.
		        // が発生する場合がある
		        if (!mPlayAlways) {
		            audioTrack.pause();
		        }
		    }
		}
    }
	/**
	 * 一時停止を解除
	 */
	@Override
    public void restart() {
        final AudioTrackCustom audioTrack = mAudioTrack;
		if (audioTrack != null) {
            if (mIsInSeek) {
                // TODO これでいいか？
                mSeekLastState = AudioTrack.PLAYSTATE_PLAYING;
                audioTrack.play();
            } else {
                if (!mPlayAlways) {
                    audioTrack.play();
                }
            }
//		    audioTrack.setPlaybackHeadPosition(mLastPlaybackHeadPosition);
		}
		mReservePause = false;
    }
	/**
	 * ダミーデータを過ぎて、本当の音声部分が再生開始するまで待つ
	 */
	@Override
    public void waitRealStartAtStart() {
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, "StreamAudioPlayer#waitRealStartAtStart()");
        }
	    AudioTrackCustom audioTrack = null;
		while (!mIsFinish) {
	        audioTrack = mAudioTrack;
			if (audioTrack != null && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
				break;
			}

			// イベントはないのでポーリングで待つ
			SystemClock.sleep(10L);
		}

		assert mAudioSampleByteSize > 0;
//        final int dummyFrame = 1;
//        final int dummyFrame = DUMMY_DATA_SIZE / mAudioSampleByteSize;
        final int dummyFrame = getWrittenFrameDummyAtStart();
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("dummyFrame=")
                    .append(dummyFrame).toString());
        }
		while (!mIsFinish) {
			// TODO: とりあえずAudioTrack開始までポーリング
		    // OnPlaybackPositionUpdateListenerの精度は微妙？
			assert audioTrack != null;
			int playbackHeadPosition = audioTrack.getPlaybackHeadPosition();
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf().append("getPlaybackHeadPosition=")
						.append(playbackHeadPosition).toString());
			}
			int diffFrame = dummyFrame - playbackHeadPosition;
			if (diffFrame <= 0) {
				break;
			}
			// sleepの誤差を考慮して待つ
			// TODO 待ち時間長いとスレッド終了チェックに間があく
			long waitTime = diffFrame * 1000L / mSampleRate;
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, Log.buf().append("waitTime=")
                        .append(waitTime).toString());
            }
			if (waitTime > 10) {
	            SystemClock.sleep(waitTime - 10L);
			} else {
			    SystemClock.sleep(1L);
			}
		}

		if (mReservePause) {
			pause();
			mReservePause = false;
		}
    }

	/**
	 * 再生が終了／完了するまで待つ
	 */
	@Override
    public void waitPlayEnd() {
		while (!mIsFinish) {
			final AudioTrack audioTrack = mAudioTrack;
			if (audioTrack == null) {
				break;
			}
			if (audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
				break;
			}
			SystemClock.sleep(33L);
		}
    }

	/**
	 * 音声バッファのサイズをバイト単位で算出
	 * @return
	 */
	@Override
    public int getAudioBufferByteSize() {
        final AudioTrackCustom audioTrack = mAudioTrack;
        int audioBufferByteSize;
        if (audioTrack == null) {
            audioBufferByteSize = AUDIO_BUFFER_BYTE_SIZE;
        } else {
    		audioBufferByteSize = audioTrack.getSampleRate() * mAudioSampleByteSize;
    		if (audioBufferByteSize < AUDIO_BUFFER_BYTE_SIZE) {
    			audioBufferByteSize = AUDIO_BUFFER_BYTE_SIZE;
    		}
        }
		return audioBufferByteSize;
    }

	/**
     * 音声バッファに格納可能な残りサイズをバイト単位で算出
	 * @return
	 */
	@Override
    public int getRemainBufferByteSize() {
        final short[] audioBufferSecond = mAudioBufferSecond;
		if (audioBufferSecond == null) {
			// 終了済み
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf()
						.append("getRemainBufferByteSize(): mAudioBufferSecond is null alreday")
						.toString());
			}
			return 0;
		}
		return (audioBufferSecond.length - mAudioBufferSecondOffset) * 2;
	}

	/**
	 * 音声データをバッファに書き込む
	 * @param buffer 音声データ
	 * @param sizeInShorts 音声データのサイズ（short単位）
	 */
	@Override
    public void writeBuffer(short[] buffer, int sizeInShorts) {
        final int sizeInBytes = sizeInShorts * 2;
		assert sizeInShorts >= 0;
		assert sizeInShorts <= buffer.length;
        final short[] audioBufferSecond = mAudioBufferSecond;
		if (audioBufferSecond == null) {
			// 終了済み
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf()
						.append("writeBuffer(): mAudioBufferSecond is null alreday")
						.toString());
			}
			return;
		}
		if (audioBufferSecond.length - mAudioBufferSecondOffset < sizeInShorts) {
			// TODO 空きが足らないのでデバイスに流す→一時停止でデバイスは止まっているのにデコーダが動き続けるのが悪い？
			flushBufferToAudioTrack();
		}
		System.arraycopy(buffer, 0, audioBufferSecond, mAudioBufferSecondOffset, sizeInShorts);
		mAudioBufferSecondOffset += sizeInShorts;
		mWrittenBufferByteSize += sizeInBytes;
	}
    @Override
    public void writeBuffer(ByteBuffer buffer, int sizeInShorts) {
        final int sizeInBytes = sizeInShorts * 2;
        assert sizeInShorts >= 0;
        assert sizeInBytes <= buffer.capacity();
        final short[] audioBufferSecond = mAudioBufferSecond;
        if (audioBufferSecond == null) {
            // 終了済み
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, Log.buf()
                        .append("writeBuffer(): mAudioBufferSecond is null alreday")
                        .toString());
            }
            return;
        }
        if (audioBufferSecond.length - mAudioBufferSecondOffset < sizeInShorts) {
            // TODO 空きが足らないのでデバイスに流す→一時停止でデバイスは止まっているのにデコーダが動き続けるのが悪い？
            flushBufferToAudioTrack();
        }
        FFmpegVideoDecoder.copyDirect(buffer, 0,
                audioBufferSecond, mAudioBufferSecondOffset, sizeInBytes);
        mAudioBufferSecondOffset += sizeInShorts;
        mWrittenBufferByteSize += sizeInBytes;
    }

	@Override
    public void writeBufferOnlyPool(short[] buffer, int sizeInShorts) {
        final int sizeInBytes = sizeInShorts * 2;
        assert sizeInShorts >= 0;
        assert sizeInShorts <= buffer.length;
        final short[] audioBufferSecond = mAudioBufferSecond;
        if (audioBufferSecond == null) {
            // 終了済み
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, Log.buf()
                        .append("writeBufferOnlyPool(): mAudioBufferSecond is null alreday")
                        .toString());
            }
            return;
        }
        if (audioBufferSecond.length - mAudioBufferSecondOffset < sizeInShorts) {
            // 空きが足らないときは先頭削る
            // TODO 処理無駄に重いかも
            int removeSize = sizeInShorts - (audioBufferSecond.length - mAudioBufferSecondOffset);
            System.arraycopy(audioBufferSecond, removeSize, audioBufferSecond, 0, audioBufferSecond.length - removeSize);
            mAudioBufferSecondOffset = audioBufferSecond.length - removeSize;
        }
        System.arraycopy(buffer, 0, audioBufferSecond, mAudioBufferSecondOffset, sizeInShorts);
        mAudioBufferSecondOffset += sizeInShorts;
        mWrittenBufferByteSize += sizeInBytes;
    }

	/**
	 * 音声バッファを{@link android.media.AudioTrack}に転送
	 */
	@Override
    public void flushBufferToAudioTrack() {
	    final short[] audioBufferSecond = mAudioBufferSecond;
        final AudioTrackCustom audioTrack = mAudioTrack;
		if (audioBufferSecond == null || audioTrack == null) {
			// 終了済み
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf()
						.append("flushBufferToAudioTrack(): already null : mAudioBufferSecond=")
						.append(audioBufferSecond).append(" mAudioTrack=")
						.append(audioTrack)
						.toString());
			}
			return;
		}
    	int writtenSize = audioTrack.write(audioBufferSecond, 0, mAudioBufferSecondOffset);
    	if (writtenSize < 0) {
    		Log.d(LOG_TAG, Log.buf().append("AudioTrack write NG: ")
    				.append(writtenSize).toString());
    		return;
    	}
    	if (DEBUG_LOGV) {
    		Log.v(LOG_TAG, Log.buf().append("writtenSize=")
    				.append(writtenSize).toString());
    	}
		mWrittenDeviceByteSize += writtenSize * 2;
        int result = audioTrack.setNotificationMarkerPosition(getWrittenFrameAll());
        assert result == AudioTrack.SUCCESS;
    	if (writtenSize == mAudioBufferSecondOffset) {
    		mAudioBufferSecondOffset = 0;
    	} else {
    		assert writtenSize < mAudioBufferSecondOffset;
    		System.arraycopy(audioBufferSecond, writtenSize,
    		        audioBufferSecond, 0,
    				mAudioBufferSecondOffset - writtenSize);
    		mAudioBufferSecondOffset -= writtenSize;
    	}
	}
	@Override
    public void flushAudioTrack() {
        final AudioTrackCustom audioTrack = mAudioTrack;
        assert audioTrack != null;
        if (audioTrack != null) {
            audioTrack.flush();
        }
    }

	/**
	 * キャッシュ済みの音声サイズをshort単位で取得
	 * @return
	 */
	@Override
    public int getCachedSize() {
		return mAudioBufferSecondOffset;
	}
	/**
	 * 音声バッファに十分なキャッシュがあるか判定
	 * @return
	 */
	@Override
    public boolean hasEnoughCache() {
        final short[] audioBufferSecond = mAudioBufferSecond;
        final AudioTrackCustom audioTrack = mAudioTrack;
		if (audioBufferSecond == null || audioTrack == null) {
			// 終了済み
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf()
						.append("hasEnoughCache(): already null : mAudioBufferSecond=")
                        .append(audioBufferSecond).append(" mAudioTrack=")
                        .append(audioTrack)
						.toString());
			}
			return false;
		}
		int enough = audioTrack.getSampleRate() * mAudioSampleByteSize * 25 / 100;
		if (enough > audioBufferSecond.length * 1 / 2) {
			enough = audioBufferSecond.length * 1 / 2;
		}
		return mAudioBufferSecondOffset > enough;
	}
	@Override
    public int getAudioSampleByteSize() {
		return mAudioSampleByteSize;
	}
	@Override
    public int getSampleRate() {
        final AudioTrackCustom audioTrack = mAudioTrack;
		if (audioTrack != null) {
			return audioTrack.getSampleRate();
		} else {
			return 0;
		}
	}

	/**
	 * 現在の再生位置を{@link Rational}で取得<br>
	 * 現状シークすると若干誤差が発生する
	 * @param rational
	 */
	@Override
    public void getCurrentPosition(Rational rational) {
        final AudioTrackCustom audioTrack = mAudioTrack;
		if (audioTrack != null) {
		    try {
    			final int dummyFrame = getWrittenFrameDummy();
    			int pos = (audioTrack.getPlaybackHeadPosition() - dummyFrame);
    			pos -= mLastWrittenDeviceByteSizeAtSeek / mAudioSampleByteSize;
    			int sampleRate = audioTrack.getSampleRate();
    			pos += (long) mOffsetTimeMs * sampleRate / 1000L;
    			if (pos >= 0) {
    				rational.num = pos;
    			} else {
    				rational.num = 0;
    			}
    			rational.den = sampleRate;
		    } catch (IllegalStateException e) {
		        Log.w(LOG_TAG, e.toString(), e);
		        // releaseが割り込むとnativeのリソースが先に解放されて例外が起きる可能性がある
		        // →ここでは無視して0返す
	            rational.num = 0;
	            rational.den = 1;
		    }
		} else {
			rational.num = 0;
			rational.den = 1;
		}
	}

	/**
	 * デバイス上に再生待ちで残っている音声データのサイズを取得
	 * @return
	 */
	@Override
    public int getRemainDataSizeOnDeviceMs() {
	    // TODO シーク発生時の計算方法どうするか
	    final AudioTrackCustom audioTrack = mAudioTrack;
		if (audioTrack != null) {
			final int dummyFrame = getWrittenFrameDummy();
			int currentFrame = (audioTrack.getPlaybackHeadPosition() - dummyFrame);
			int remainFrame = getWrittenFrameAudio() - currentFrame;
			assert remainFrame >= 0;
			return (int) ((long) remainFrame * 1000L / audioTrack.getSampleRate());
		} else {
			return 0;
		}
	}

//	/**
//	 * 音声バッファと{@link android.media.AudioTrack}のクリア
//	 */
//	public void clearBuffer() {
//    	mAudioBufferSecondOffset = 0;
//        final AudioTrackCustom audioTrack = mAudioTrack;
//		if (audioTrack != null) {
//	    	final int lastState = audioTrack.getPlayState();
//	    	audioTrack.stop();
////	    	assert audioTrack.getPlaybackHeadPosition() == 0;
//	    	audioTrack.play();
//	    	if (lastState == AudioTrack.PLAYSTATE_PAUSED) {
//	    	    audioTrack.pause();
//	    	}
//		}
//	}

    @Override
    public void startSeek() {
        mIsInSeek = true;
        mSeekLastDummyStartTime = SystemClock.uptimeMillis();
        mAudioBufferSecondOffset = 0;
        mLastWrittenDeviceByteSizeAtSeek = mWrittenDeviceByteSize;
        AudioTrackCustom audioTrack = mAudioTrack;
        if (audioTrack != null) {
            mSeekLastState = audioTrack.getPlayState();

            float minVolume = AudioTrack.getMinVolume();
            audioTrack.setStereoVolume(minVolume, minVolume);
            if (mSeekLastState == AudioTrack.PLAYSTATE_PAUSED) {
                // 完全停止を避けるため一時停止中でも動作させる
                audioTrack.play();
            }
        }
    }

    @Override
    public void endSeek(int currentMilliSecond) {
        final AudioTrackCustom audioTrack = mAudioTrack;
        if (audioTrack != null) {
            if (mSeekLastState == AudioTrack.PLAYSTATE_PAUSED) {
                audioTrack.pause();
            }
            if (mReservePause) {
                audioTrack.pause();
                mReservePause = false;
            }
            float maxVolume = AudioTrack.getMaxVolume();
            audioTrack.setStereoVolume(maxVolume, maxVolume);
        }
        mOffsetTimeMs = currentMilliSecond;
        mIsInSeek = false;
    }

	/**
	 * ダミーデータ再生中に一時停止を予約
	 */
	@Override
    public void reservePause() {
		mReservePause = true;
	}

	private byte[] getDummyBuffer(int size) {
	    byte[] buf = mRefDummyBuffer.get();
	    if (buf == null || buf.length < size) {
	        buf = new byte[size];
//	        Arrays.fill(buf, (byte) 0);
	        mRefDummyBuffer = new SoftReference<byte[]>(buf);
	    }
	    return buf;
	}

    private AudioTrackCustom createAudioTrackCustom(int sampleRateInHz,
            int channelConfig, int audioFormat, int bufferSize) {
        for (int i = 0; i < 3; ++i) {
            AudioTrackCustom audioTrack = new AudioTrackCustom(
                    AudioManager.STREAM_MUSIC,
                    sampleRateInHz,
                    channelConfig,
                    audioFormat,
                    bufferSize,
                    AudioTrack.MODE_STREAM);
            int state = audioTrack.getState();
            if (state != AudioTrack.STATE_UNINITIALIZED) {
                assert state == AudioTrack.STATE_INITIALIZED;
                if (mPlayAlways) {
                    mLooperThread = new LooperThread("StreamAudioPlayer-Listener") {
                        @Override
                        public boolean handleMessage(Message msg) {
                            final AudioTrackCustom audioTrack = mAudioTrack;
                            Handler handler = getHandler();
                            if (audioTrack != null && getRemainDataSizeOnDeviceMs() == 0) {
                                int dummyByte = (int) (PLAY_ALWAYS_INTERVAL_MS * mSampleRate / 1000L * mAudioSampleByteSize);
                                int written = writeDummyDataCommon(audioTrack, dummyByte);
                                if (written >= 0) {
                                    // TODO
                                    mDummyDataByteSizeAtSeek += written;
                                    int result = audioTrack.setNotificationMarkerPosition(getWrittenFrameAll());
                                    assert result == AudioTrack.SUCCESS;
                                }
                            } else {
                                handler.removeMessages(0);
                            }
                            return true;
                        }
                    };
                    mLooperThread.start();
                    audioTrack.setPlaybackPositionUpdateListener(this, mLooperThread.getHandler());
                }
                return audioTrack;
            }
            audioTrack = null;
            Util.gcStrong();
            SystemClock.sleep(100L);
        }
        throw new IllegalStateException("Create AudioTrack failed");
    }

    private int getWrittenFrameDummyAtStart() {
        return (int) (mDummyDataByteSizeAtStart / mAudioSampleByteSize);
    }
    private int getWrittenFrameDummy() {
        return (int) ((mDummyDataByteSizeAtStart + mDummyDataByteSizeAtSeek)
                / mAudioSampleByteSize);
    }
    private int getWrittenFrameAudio() {
        return (int) (mWrittenDeviceByteSize / mAudioSampleByteSize);
    }
    private int getWrittenFrameAll() {
        return (int) ((mDummyDataByteSizeAtStart + mWrittenDeviceByteSize
                + mDummyDataByteSizeAtSeek) / mAudioSampleByteSize);
    }

    @Override
    public void onMarkerReached(AudioTrack track) {
        try {
            int marker = track.getNotificationMarkerPosition();
            int writtenFrame = getWrittenFrameAll();
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, Log.buf().append("StreamAudioPlayer#onMarkerReached: marker=")
                        .append(marker).append(" written=").append(writtenFrame).toString());
            }
            if (mLooperThread != null) {
                Handler handler = mLooperThread.getHandler();
                handler.removeMessages(0);
                handler.sendEmptyMessageDelayed(0, PLAY_ALWAYS_INTERVAL_MS);
            }
        } catch (IllegalStateException e) {
            Log.w(LOG_TAG, e.toString(), e);
        }
    }

    @Override
    public void onPeriodicNotification(AudioTrack track) {
    }
}
