package jp.sourceforge.nicoro;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;

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

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.os.StatFs;
import android.preference.PreferenceManager;


public class VideoLoader implements Runnable, FFmpegIOCallback {
	private static final boolean DEBUG_LOGD = Release.IS_DEBUG && true;
	private static final boolean DEBUG_LOGD_READ = Release.IS_DEBUG && false;
	private static final boolean DEBUG_LOGD_SEEK = Release.IS_DEBUG && false;
	
	public static final String STREAM_TEMP_DEVICE_ROOT =
		Environment.getExternalStorageDirectory().getAbsolutePath();
	public static final String STREAM_TEMP_DIR =
		STREAM_TEMP_DEVICE_ROOT + "/NicoRo/";
	
	/**
	 * キャッシュに必要な最小限の空きサイズ
	 */
	private static final long NEEDED_FREE_DISK_SIZE = 101 * 1024 * 1024;
	
	private static final int SAVE_INFO_INTERVAL_SIZE = 1 * 1024 * 1024;
	
	private DefaultHttpClient mHttpClient;
	private String mUrl;
	private String mCookie;
	private String mVideoNumber;
	private Context mContext;
	private String mCookieUserSession;
	private byte[] mBuffer = new byte[1024*8];
	private int mBufferOffset = 0;
	private volatile boolean mIsFinish = false;
	private Thread mThread = new Thread(this);
	private long mContentLength = -1;
	private boolean mIsStarted = false;
	
	private long mSeekOffsetWrite = 0;
	private long mSeekOffsetRead = 0;
	private int mBufferOffsetForNative = 0;
	private RandomAccessFile mTempFile;
	
	private long mLastSaveInfoSize = 0;
	
	private EventListener mEventListener = null;
	private boolean mIsCachedCalled = false;
	
	private String mTempFilePath;
	private String mTempInfoPath;
	
	private static class InfoData {
		String ifModifiedSince;
		String ifRange;
		String lastContentLength;
		String lastCacheLength;
		
		public void reset() {
			ifModifiedSince = null;
			ifRange = null;
			lastContentLength = null;
			lastCacheLength = null;
		}
	}
	private InfoData mTempInfoData = new InfoData();
	
	private long mCacheSize;
	private String mUserAgent;
	
	public VideoLoader(String url, String cookie, String videoNumber,
			Context context, String cookieUserSession) {
		mHttpClient = Util.createHttpClient();
		mUrl = url;
		mCookie = cookie;
		mVideoNumber = videoNumber;
		mContext = context;
		mCookieUserSession = cookieUserSession;
		
		File dir = new File(STREAM_TEMP_DIR);
		dir.mkdirs();
//		mTempFilePath = createCacheFilePath(url);
		mTempFilePath = createCacheFilePath(videoNumber);
		mTempInfoPath = createCacheInfoPath(videoNumber);
		
		SharedPreferences sharedPreference = PreferenceManager.getDefaultSharedPreferences(mContext);
		mCacheSize = 1024 * 1024 * Long.parseLong(sharedPreference.getString(
				mContext.getString(R.string.pref_key_strage_cache_size),
				"512"));
		mUserAgent = sharedPreference.getString(NicoroConfig.USER_AGENT, null);
	}
	
	public void startLoad() {
		if (mIsStarted) {
			Log.d(LOG_TAG, "it has started");
			return;
		}
		mIsStarted = true;
		mIsFinish = false;
		mSeekOffsetWrite = 0;
		mSeekOffsetRead = 0;
		mContentLength = -1;
		mThread.start();
	}
	
	public void finish() {
		mIsFinish = true;
		try {
			mThread.join(2000L);
		} catch (InterruptedException e) {
			Log.d(LOG_TAG, e.getMessage(), e);
		}
		mIsStarted = false;

		synchronized (this) {
			if (mTempFile != null) {
				try {
					mTempFile.close();
					mTempFile = null;
				} catch (IOException e) {
					Log.d(LOG_TAG, e.getMessage(), e);
				}
			}
		}
	}
	
	public boolean hasCache() {
		synchronized (this) {
			return mSeekOffsetWrite > 512 * 1024;
		}
	}
	
	public void setEventListener(EventListener eventListener) {
		mEventListener = eventListener;
	}
	
	public String getFilePath() {
		return mTempFilePath;
	}
	
	public String getVideoNumber() {
		return mVideoNumber;
	}
	
	private static String createCacheFilePath(String videoNumber) {
		return STREAM_TEMP_DIR + videoNumber + ".dat";
	}
	private static String createCacheInfoPath(String videoNumber) {
		return STREAM_TEMP_DIR + videoNumber + ".json";
	}
	
	public static boolean isStreamTempDirWritable() {
		String state = Environment.getExternalStorageState();
		return Environment.MEDIA_MOUNTED.equals(state);
	}
	
	public static boolean isStreamTempDirReadable() {
		String state = Environment.getExternalStorageState();
		return Environment.MEDIA_MOUNTED.equals(state)
			|| Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
	}
	
	private static void deleteCacheFile(long cacheSize) {
		// サイズ総計算
		File dir = new File(STREAM_TEMP_DIR);
		
		LinkedList<File> filesDat;
		File[] filesDatArray = dir.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String filename) {
				return filename.endsWith(".dat");
			}
		});
		if (filesDatArray == null) {
			filesDat = new LinkedList<File>();
		} else {
			filesDat = new LinkedList<File>(Arrays.asList(filesDatArray));
		}
		filesDatArray = null;
		
		LinkedList<File> filesJson;
		File[] filesJsonArray = dir.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String filename) {
				return filename.endsWith(".json");
			}
		});
		if (filesJsonArray == null) {
			filesJson = new LinkedList<File>();
		} else {
			filesJson = new LinkedList<File>(Arrays.asList(filesJsonArray));
		}
		filesJsonArray = null;
		
		// 整合性取れていないファイルは最優先で消す
		LOOP_DAT : for (Iterator<File> it = filesDat.iterator(); it.hasNext(); ) {
			File dat = it.next();
			String jsonName = dat.getName().replace(".dat", ".json");
			for (File json : filesJson) {
				if (jsonName.equals(json.getName())) {
					// found
					continue LOOP_DAT;
				}
			}
			// not found
			if (!dat.delete()) {
				Log.w(LOG_TAG, Log.buf().append("delete failed: ")
						.append(dat.getAbsolutePath()).toString());
			}
			it.remove();
		}
		LOOP_JSON : for (Iterator<File> it = filesJson.iterator(); it.hasNext(); ) {
			File json = it.next();
			String datName = json.getName().replace(".json", ".dat");
			for (File dat : filesDat) {
				if (datName.equals(dat.getName())) {
					// found
					continue LOOP_JSON;
				}
			}
			// not found
			if (!json.delete()) {
				Log.w(LOG_TAG, Log.buf().append("delete failed: ")
						.append(json.getAbsolutePath()).toString());
			}
			it.remove();
		}
		
		long totalCachedFileSize = 0;
		for (File f : filesDat) {
			totalCachedFileSize += f.length();
		}
		for (File f : filesJson) {
			totalCachedFileSize += f.length();
		}
		
		// 空き容量に余裕がないときはさらに余分に消す
		StatFs stat = new StatFs(STREAM_TEMP_DEVICE_ROOT);
		long freeSize = (long) stat.getBlockSize() * (long) stat.getAvailableBlocks();
		if (DEBUG_LOGD) {
			Log.d(LOG_TAG, Log.buf().append("cacheSize=").append(cacheSize)
					.append(" freeSize=").append(freeSize)
					.append(" totalCachedFileSize=").append(totalCachedFileSize).toString());
		}
		if (freeSize - (cacheSize - totalCachedFileSize)
				< NEEDED_FREE_DISK_SIZE) {
			long newCacheSize = totalCachedFileSize + freeSize - NEEDED_FREE_DISK_SIZE;
			if (newCacheSize < 0) {
				newCacheSize = 0;
			}
			assert cacheSize > newCacheSize;
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf().append("newCacheSize=").append(newCacheSize).toString());
			}
			cacheSize = newCacheSize;
		}
		
		if (totalCachedFileSize <= cacheSize) {
			// 指定キャッシュサイズ以下
			return;
		}
		
		List<JSONObject> jsons = new ArrayList<JSONObject>(filesJson.size());
		for (File json : filesJson) {
			jsons.add(Util.createJSONFromFile(json.getPath()));
		}
		
		while (totalCachedFileSize > cacheSize) {
			int oldestPos = -1;
			File oldestJson = null;
			long oldestTime = Long.MAX_VALUE;
			final int jsonsSize = jsons.size();
			for (int i = 0; i < jsonsSize; ++i) {
				long lastPlayed;
				try {
					lastPlayed = jsons.get(i).getLong("Last-Played");
					if (lastPlayed < oldestTime) {
						oldestPos = i;
						oldestJson = filesJson.get(oldestPos);
						oldestTime = lastPlayed;
					}
				} catch (JSONException e) {
					// 壊れたファイルなので最優先で消す
					oldestPos = i;
					oldestJson = filesJson.get(oldestPos);
					break;
				}
			}
			assert oldestJson != null;
			
			String datName = oldestJson.getName().replace(".json", ".dat");
			for (Iterator<File> it = filesDat.iterator(); it.hasNext(); ) {
				File dat = it.next();
				if (datName.equals(dat.getName())) {
					totalCachedFileSize -= dat.length();
					if (!dat.delete()) {
						Log.w(LOG_TAG, Log.buf().append("delete failed: ")
								.append(dat.getAbsolutePath()).toString());
					}
					it.remove();
					break;
				}
			}
			
			totalCachedFileSize -= oldestJson.length();
			if (!oldestJson.delete()) {
				Log.w(LOG_TAG, Log.buf().append("delete failed: ")
						.append(oldestJson.getAbsolutePath()).toString());
			}
			filesJson.remove(oldestJson);
			jsons.remove(oldestPos);
		}
	}
	
	public static void deleteAllCacheFile() {
		File dir = new File(STREAM_TEMP_DIR);
		File[] filesArray = dir.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String filename) {
				return filename.endsWith(".dat")
					|| filename.endsWith(".json");
			}
		});
		if (filesArray == null) {
			// キャッシュファイル無し
			return;
		}
		for (File f : filesArray) {
			if (!f.delete()) {
				Log.w(LOG_TAG, Log.buf().append("delete failed: ")
						.append(f.getAbsolutePath()).toString());
			}
		}
	}
	
	
	@Override
	public void run() {
		if (DEBUG_LOGD) {
			Log.d(LOG_TAG, "VideoLoader run");
		}
		
		// ストレージデバイス書き込み不可の時はエラー
		if (!isStreamTempDirWritable()) {
			if (mEventListener != null) {
				String errorMessage = mContext.getString(R.string.errormessage_player_strage_unmount);
				mEventListener.onOccurredError(this, errorMessage);
			}
			return;
		}
		
		// 先にキャッシュファイル削除
		deleteCacheFile(mCacheSize);
		
		// 情報読み込み
		readInfo();
		
		loadMain(true);
		
		if (DEBUG_LOGD) {
			Log.d(LOG_TAG, "VideoLoader run end");
		}
	}
	
	private void loadMain(boolean tryRetry) {
		HttpUriRequest httpRequest = createRequest();
		if (DEBUG_LOGD) {
			Util.logHeaders(LOG_TAG, httpRequest.getAllHeaders());
		}
		
		mHttpClient.getCookieStore().clear();
		InputStream inDownload = null;
		assert mTempFile == null;
		mTempFile = null;
		
		String lastModified = null;
		String etag = null;
		try {
			HttpResponse httpResponse = mHttpClient.execute(
					httpRequest
					);
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, "==========VideoLoader run httpResponse==========");
				Log.d(LOG_TAG, httpResponse.getStatusLine().getReasonPhrase());
				Util.logHeaders(LOG_TAG, httpResponse.getAllHeaders());
				Log.d(LOG_TAG, "==========httpResponse end==========");
			}
			boolean startDownload;
			int httpStatusCode = httpResponse.getStatusLine().getStatusCode();
			if (httpStatusCode == HttpStatus.SC_NOT_MODIFIED) {
				// 更新無し、現在のファイルそのまま使用
				synchronized (this) {
					mSeekOffsetWrite = Long.parseLong(mTempInfoData.lastCacheLength);
					mContentLength = Long.parseLong(mTempInfoData.lastContentLength);
					assert mSeekOffsetWrite == mContentLength;
					mTempFile = new RandomAccessFile(mTempFilePath, "r");
				}
				lastModified = mTempInfoData.ifModifiedSince;
				etag = mTempInfoData.ifRange;
				startDownload = false;

				if (mEventListener != null) {
					if (!mIsCachedCalled) {
						mEventListener.onCached(this);
						mIsCachedCalled = true;
					}
					mEventListener.onFinished(this);
				}
			} else if (httpStatusCode == HttpStatus.SC_PARTIAL_CONTENT) {
				// 続きから取得
				synchronized (this) {
					mSeekOffsetWrite = Long.parseLong(mTempInfoData.lastCacheLength);
					mContentLength = Long.parseLong(mTempInfoData.lastContentLength);
					assert mSeekOffsetWrite < mContentLength;
					mTempFile = new RandomAccessFile(mTempFilePath, "rw");
				}
				lastModified = Util.getFirstHeaderValue(httpResponse, "Last-Modified");
				etag = Util.getFirstHeaderValue(httpResponse, "ETag");
				startDownload = true;
			} else if (httpStatusCode == HttpStatus.SC_OK) {
				// 最初から取得
				synchronized (this) {
					mSeekOffsetWrite = 0;
					mContentLength = Long.parseLong(
							Util.getFirstHeaderValue(httpResponse, "Content-Length"));
					if (DEBUG_LOGD) {
						Log.d(LOG_TAG, Log.buf().append("mContentLength=")
								.append(mContentLength).toString());
					}
					mTempFile = new RandomAccessFile(mTempFilePath, "rw");
					// 事前にファイルサイズ変更
					mTempFile.setLength(mContentLength);
				}
				lastModified = Util.getFirstHeaderValue(httpResponse, "Last-Modified");
				etag = Util.getFirstHeaderValue(httpResponse, "ETag");
				startDownload = true;
			} else if (httpStatusCode == HttpStatus.SC_FORBIDDEN) {
				// Cookie取り直してもう１回再取得を試みる
				if (tryRetry) {
					mHttpClient.getCookieStore().clear();
					mCookie = NicoroAPIManager.getCookieNicoHistory(
							mHttpClient, mVideoNumber, mCookieUserSession, mUserAgent);
					loadMain(false);
					return;
				} else {
					// エラー
					if (mEventListener != null) {
						String errorMessage = "HTTP Status Code: " + httpStatusCode;
						mEventListener.onOccurredError(this, errorMessage);
					}
					startDownload = false;
				}
			} else {
				// エラー
				if (mEventListener != null) {
					String errorMessage = "HTTP Status Code: " + httpStatusCode;
					mEventListener.onOccurredError(this, errorMessage);
				}
				startDownload = false;
			}
			
			if (startDownload) {
				// 読み込み
				HttpEntity httpEntity = httpResponse.getEntity();
				inDownload = httpEntity.getContent();
				while (!mIsFinish) {
					synchronized (this) {
						int read = inDownload.read(mBuffer, 0, mBuffer.length);
						if (read < 0) {
							break;
						}
						mTempFile.seek(mSeekOffsetWrite);
						mTempFile.write(mBuffer, 0, read);
						mSeekOffsetWrite += read;
//						Log.d(LOG_TAG, "buffer read=" + read + " mSeekOffsetWrite=" + mSeekOffsetWrite);
					}
					if (mSeekOffsetWrite >= mLastSaveInfoSize + SAVE_INFO_INTERVAL_SIZE) {
						// 一定サイズごとに情報保存（強制終了に備えて）
						saveInfo(lastModified, etag);
						mLastSaveInfoSize = mSeekOffsetWrite;
					}
					if (mEventListener != null) {
						mEventListener.onNotifyProgress((int) mSeekOffsetWrite, (int) mContentLength);
						if (!mIsCachedCalled && hasCache()) {
							mEventListener.onCached(this);
							mIsCachedCalled = true;
						}
					}
					try {
						Thread.sleep(10L);
					} catch (InterruptedException e) {
					}
				}
				if (DEBUG_LOGD) {
					Log.d(LOG_TAG, "VideoLoader load finished");
				}
				if (mSeekOffsetWrite == mContentLength) {
					if (mEventListener != null) {
						// 読み込み完了でもキャッシュコールバック
						if (!mIsCachedCalled) {
							mEventListener.onCached(this);
							mIsCachedCalled = true;
						}
						mEventListener.onFinished(this);
					}
				}
			}
		} catch (ClientProtocolException e) {
			if (mEventListener != null) {
				String errorMessage = e.getMessage();
				if (errorMessage == null) {
					errorMessage = "ClientProtocolException";
				}
				mEventListener.onOccurredError(this, errorMessage);
			}
			Log.d(LOG_TAG, e.getMessage(), e);
		} catch (IOException e) {
			if (mEventListener != null) {
				String errorMessage = e.getMessage();
				if (errorMessage == null) {
					errorMessage = "IOException";
				}
				mEventListener.onOccurredError(this, errorMessage);
			}
			Log.d(LOG_TAG, e.getMessage(), e);
		} finally {
			if (inDownload != null) {
				try {
					inDownload.close();
				} catch (IOException e) {
					Log.d(LOG_TAG, e.getMessage(), e);
				}
			}
			
			// 情報保存
			if (lastModified != null && etag != null) {
				saveInfo(lastModified, etag);
			}
		}
	}
	
	private void readInfo() {
		JSONObject jsonLoad = Util.createJSONFromFile(mTempInfoPath);
		mTempInfoData.reset();
		if (jsonLoad == null) {
			// ない場合はそのまま処理継続
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf().append(mTempInfoPath)
						.append(" load failed, continue.").toString());
			}
		} else {
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf().append("Load ").append(mTempInfoPath)
						.append(": ").append(jsonLoad.toString()).toString());
			}
			try {
				mTempInfoData.ifModifiedSince = jsonLoad.getString("Last-Modified");
				mTempInfoData.ifRange = jsonLoad.getString("ETag");
				mTempInfoData.lastContentLength = jsonLoad.getString("Content-Length");
				mTempInfoData.lastCacheLength = jsonLoad.getString("Cached-Length");
			} catch (JSONException e) {
				Log.d(LOG_TAG, e.getMessage(), e);
			}
		}
	}
	
	private void saveInfo(String lastModified, String etag) {
		assert lastModified != null;
		assert etag != null;
		assert mContentLength > 0;
		assert mSeekOffsetWrite >= 0;
		
		try {
			JSONObject jsonSave = new JSONObject();
			jsonSave.put("Content-Length", mContentLength)
				.put("Last-Modified", lastModified)
				.put("ETag", etag)
				.put("Cached-Length", mSeekOffsetWrite)
				.put("Last-Played", System.currentTimeMillis());
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf().append("Try save ").append(mTempInfoPath)
						.append(": ").append(jsonSave.toString()).toString());
			}
			if (!Util.saveJSONToFile(mTempInfoPath, jsonSave)) {
				Log.w(LOG_TAG, Log.buf().append(mTempInfoPath)
						.append(" save failed.").toString());
			}
		} catch (JSONException e) {
			Log.d(LOG_TAG, e.getMessage(), e);
		}
	}

	@Override
	public int readFromNativeCallback(int bufSize, byte[] buffer) {
//		byte[] buffer = mBufferForNative;
		
		boolean readWait;
		synchronized (this) {
			if (DEBUG_LOGD_READ) {
				Log.d(LOG_TAG, Log.buf()
						.append("readFromNativeCallback: bufSize=").append(bufSize)
						.append(" buffer=").append(buffer.toString())
						.append(" mSeekOffsetRead=").append(mSeekOffsetRead)
						.append(" mSeekOffsetWrite=").append(mSeekOffsetWrite).toString());
			}
			readWait = (mSeekOffsetRead > mSeekOffsetWrite);
		}
		while (readWait) {
			try {
				Thread.sleep(10L);
			} catch (InterruptedException e) {
			}
			synchronized (this) {
				readWait = (mSeekOffsetRead > mSeekOffsetWrite);
			}
		}
		
		int ret;
		synchronized (this) {
			try {
				mTempFile.seek(mSeekOffsetRead);
				if (mSeekOffsetRead + bufSize > mSeekOffsetWrite) {
					bufSize = (int) (mSeekOffsetWrite - mSeekOffsetRead);
				}
				if (bufSize > buffer.length) {
					bufSize = buffer.length;
				}
				int read = mTempFile.read(buffer, 0, bufSize);
				if (read >= 0) {
					mSeekOffsetRead += read;
				}
				ret = read;
			} catch (IOException e) {
				Log.d(LOG_TAG, e.getMessage(), e);
				ret = -1;
			}
		}
		if (DEBUG_LOGD_READ) {
			Log.d(LOG_TAG, Log.buf().append("readFromNativeCallback: return=")
					.append(ret).toString());
		}
		return ret;
	}
	
	private HttpUriRequest createRequest() {
		HttpUriRequest httpRequest = new HttpGet(mUrl);
		httpRequest.addHeader("Cookie", mCookie);
		if (mUserAgent != null) {
			httpRequest.setHeader("User-Agent", mUserAgent);
		}
		
		if (mTempInfoData.lastContentLength != null && mTempInfoData.lastContentLength.equals(mTempInfoData.lastCacheLength)) {
			// 更新チェックを試みる
			if (mTempInfoData.ifModifiedSince != null) {
				httpRequest.addHeader("If-Modified-Since", mTempInfoData.ifModifiedSince);
			}
		} else {
			if (mTempInfoData.ifRange != null && mTempInfoData.lastCacheLength != null) {
				// 継続ダウンロードを試みる
				httpRequest.addHeader("If-Range", mTempInfoData.ifRange);
				httpRequest.addHeader("Range", "bytes= " + mTempInfoData.lastCacheLength + "-");
			} else {
				// 新規ダウンロード
			}
		}
		return httpRequest;
	}
	
	public static final boolean isFinishedCache(String videoNumber) {
		String tempInfoPath = createCacheInfoPath(videoNumber);
		JSONObject jsonLoad = Util.createJSONFromFile(tempInfoPath);
		if (jsonLoad == null) {
			return false;
		}
		try {
			String lastContentLength = jsonLoad.getString("Content-Length");
			String lastCacheLength = jsonLoad.getString("Cached-Length");
			if (lastContentLength != null && lastContentLength.equals(lastCacheLength)) {
				// TODO とりあえず更新チェック省略
				return true;
			} else {
				return false;
			}
		} catch (JSONException e) {
			Log.d(LOG_TAG, e.getMessage(), e);
			return false;
		}
	}
	
	private static final int SEEK_SET = 0;
	private static final int SEEK_CUR = 1;
	private static final int SEEK_END = 2;
	private static final int AVSEEK_SIZE = 0x10000;

	@Override
	public long seekFromNativeCallback(long offset, int whence) {
		long ret = -1L;
		synchronized (this) {
			if (DEBUG_LOGD_SEEK) {
				Log.d(LOG_TAG, Log.buf()
						.append("seekFromNativeCallback: offset=").append(offset)
						.append(" whence=").append(whence)
						.append(" mSeekOffsetRead=").append(mSeekOffsetRead).toString());
			}
			long seekOffsetRead;
			switch (whence) {
			case SEEK_SET:
				seekOffsetRead = offset;
				if (mContentLength >= 0) {
					if (seekOffsetRead >= 0 && seekOffsetRead <= mContentLength) {
						mSeekOffsetRead = seekOffsetRead;
						ret = mSeekOffsetRead;
					}
				} else {
					mSeekOffsetRead = seekOffsetRead;
					ret = mSeekOffsetRead;
				}
				break;
			case SEEK_CUR:
				seekOffsetRead = mSeekOffsetRead + offset;
				if (mContentLength >= 0) {
					if (seekOffsetRead >= 0 && seekOffsetRead <= mContentLength) {
						mSeekOffsetRead = seekOffsetRead;
						ret = mSeekOffsetRead;
					}
				} else {
					mSeekOffsetRead = seekOffsetRead;
					ret = mSeekOffsetRead;
				}
				break;
			case SEEK_END:
				seekOffsetRead = mContentLength + offset;
				if (mContentLength >= 0) {
					if (seekOffsetRead >= 0 && seekOffsetRead <= mContentLength) {
						mSeekOffsetRead = seekOffsetRead;
						ret = mSeekOffsetRead;
					}
				}
				break;
			case AVSEEK_SIZE:
//				ret = mContentLength;
				ret = -1;
				break;
			}
		}
		if (DEBUG_LOGD_SEEK) {
			Log.d(LOG_TAG, Log.buf().append("seekFromNativeCallback: return=").append(ret).toString());
		}
		return ret;
	}
	
	public interface EventListener {
		/**
		 * ファイルの先頭部分の読み込み完了
		 * @param streamLoader
		 */
		public void onCached(VideoLoader streamLoader);
		/**
		 * ファイル全体の読み込み完了
		 * @param streamLoader
		 */
		public void onFinished(VideoLoader streamLoader);
		/**
		 * エラー発生
		 * @param streamLoader
		 * @param errorMessage
		 */
		public void onOccurredError(VideoLoader streamLoader, String errorMessage);
		/**
		 * 読み込み経過の通知
		 * @param seekOffsetWrite 分子
		 * @param contentLength 分母
		 */
		public void onNotifyProgress(int seekOffsetWrite, int contentLength);
	}
}
