package jp.sourceforge.nicoro;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
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.StatusLine;
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.apache.http.message.BasicLineFormatter;
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.os.SystemClock;
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/";

	/**
	 * キャッシュに必要な最小限の空きサイズ
	 *
	 * TODO: 公式チャンネル動画には300MB超のものもあるがどうするか？
	 */
	private static final long NEEDED_FREE_DISK_SIZE = 101 * 1024 * 1024;

	private static final int SAVE_INFO_INTERVAL_SIZE = 1 * 1024 * 1024;
	private static final int SAVE_INFO_INTERVAL_TIME_MS = 5 * 1000;

	private static final int ENOUGH_CACHE_SIZE = 1 * 1024 * 1024;

//    private static final int DOWNLOAD_BUFFER_SIZE = 4 * 1024;;
	private static final int DOWNLOAD_BUFFER_SIZE = 8 * 1024;;
//    private static final int DOWNLOAD_BUFFER_SIZE = 32 * 1024;;

	private static final String HEADER_CONTENT_LENGTH = "Content-Length";
	private static final String HEADER_LAST_MODIFIED = "Last-Modified";
	private static final String HEADER_ETAG = "ETag";
	private static final String KEY_CACHED_LENGTH = "Cached-Length";
	private static final String KEY_LAST_PLAYED = "Last-Played";
	private static final String LOW_VIDEO_SUFFIX = "_low";

	private DefaultHttpClient mHttpClient;
	private String mUrl;
	private String mCookie;
	private String mVideoNumber;
	private Context mContext;
	private String mCookieUserSession;
	private boolean mIsLow;
//	private int mBufferOffset = 0;
	private volatile boolean mIsFinish = false;
	private Thread mThread = new Thread(this);
	private long mContentLength = -1;
	private String mETag;
	private String mLastModified;
	private boolean mIsStarted = false;

//	private int mBufferOffsetForNative = 0;
	private volatile MultiRandomAccessFile mTempFile;

	private long mLastSaveInfoSize = 0;
	private long mLastSaveInfoTime = 0;

	private EventListener mEventListener = null;
	private boolean mIsCachedCalled = false;
	private boolean mIsCacheCompleted = false;

	private String mTempFilePath;
	private String mTempInfoPath;

	public static class ExternalInfoData {
        public String videoNumber;
        public long lastPlayed;
	}

	private static class InfoData {
	    String ifModifiedSince;
	    String ifRange;
	    String lastContentLength;
	    String lastCacheLength;

	    String ifModifiedSinceLow;
	    String ifRangeLow;
	    String lastContentLengthLow;
	    String lastCacheLengthLow;

		public void reset() {
			ifModifiedSince = null;
			ifRange = null;
			lastContentLength = null;
			lastCacheLength = null;

			ifModifiedSinceLow = null;
			ifRangeLow = null;
			lastContentLengthLow = null;
			lastCacheLengthLow = null;
		}
	}
	private InfoData mTempInfoData = new InfoData();

	private long mCacheSize;
	private String mUserAgent;

	private String mMovieType;

	private HttpUriRequest mHttpRequest;

	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;
		mIsLow = NicoroAPIManager.isGetflvUrlLow(url);

		File dir = new File(STREAM_TEMP_DIR);
		dir.mkdirs();
//		mTempFilePath = createCacheFilePath(url);
		if (mIsLow) {
			mTempFilePath = createLowCacheFilePath(videoNumber);
		} else {
			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;
		mContentLength = -1;
		mETag = null;
		mLastModified = null;
		mHttpRequest = null;
		mIsCacheCompleted = false;
		mThread.start();
	}

	public void finish() {
		mIsFinish = true;
		HttpUriRequest httpRequest = mHttpRequest;
		if (httpRequest != null && !httpRequest.isAborted()) {
			httpRequest.abort();
		}
		try {
			mThread.join(2000L);
		} catch (InterruptedException e) {
			Log.d(LOG_TAG, e.toString(), e);
		}
		mIsStarted = false;

		MultiRandomAccessFile tempFile = mTempFile;
		if (tempFile != null) {
			try {
				tempFile.syncWrite();
			} catch (IOException e) {
				Log.d(LOG_TAG, e.toString(), e);
			}
			try {
				tempFile.close();
				mTempFile = null;
			} catch (IOException e) {
				Log.d(LOG_TAG, e.toString(), e);
			}
		}
	}

	public synchronized boolean hasCache() {
		// TODO キャッシュサイズ計算はビットレートに応じて可変なのがベスト
		MultiRandomAccessFile tempFile = mTempFile;
		if (tempFile == null) {
			return false;
		}
		final long seekOffsetWrite = tempFile.tellWrite();
		return seekOffsetWrite > ENOUGH_CACHE_SIZE
			|| seekOffsetWrite == mContentLength;
	}

	public void setEventListener(EventListener eventListener) {
		mEventListener = eventListener;
	}

	public String getFilePath() {
		return mTempFilePath;
	}

	public String getVideoNumber() {
		return mVideoNumber;
	}

	public long getContentLength() {
		return mContentLength;
	}

	public String getETag() {
		return mETag;
	}

	public String getLastModified() {
		return mLastModified;
	}

	public long getSeekOffsetRead() {
		MultiRandomAccessFile tempFile = mTempFile;
		if (tempFile == null) {
			return 0L;
		}
		return tempFile.tellRead();
	}

	private static String createCacheFilePath(String videoNumber) {
		return STREAM_TEMP_DIR + videoNumber + ".dat";
	}
	private static String createLowCacheFilePath(String videoNumber) {
		return STREAM_TEMP_DIR + videoNumber + LOW_VIDEO_SUFFIX + ".dat";
	}
	private static String createCacheInfoPath(String videoNumber) {
		return STREAM_TEMP_DIR + videoNumber + ".json";
	}

    /**
     * 生放送用のキャッシュファイルのパスを取得
     * @return
     */
    static String createLiveCacheFilePath() {
        return STREAM_TEMP_DIR + "livetemp.dat";
    }

	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) {
		// TODO: スレッド終了フラグが立ったときに中断しなくて大丈夫か？

		// サイズ総計算
		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;

		// 整合性取れていないファイルは最優先で消す
		final String lowDatSuffix = LOW_VIDEO_SUFFIX + ".dat";
		LOOP_DAT : for (Iterator<File> it = filesDat.iterator(); it.hasNext(); ) {
			File dat = it.next();
			String jsonNameTarget = dat.getName()
				.replace(lowDatSuffix, ".json")
				.replace(".dat", ".json");
			for (File json : filesJson) {
				if (jsonNameTarget.equals(json.getName())) {
					// found
					continue LOOP_DAT;
				}
			}
			// not found
			deleteCacheFile(dat);
			it.remove();
		}
		LOOP_JSON : for (Iterator<File> it = filesJson.iterator(); it.hasNext(); ) {
			File json = it.next();
			final String jsonName = json.getName();
			String datNameTarget = jsonName.replace(".json", ".dat");
			String lowDatNameTarget = jsonName.replace(".json", lowDatSuffix);
			for (File dat : filesDat) {
				String datName = dat.getName();
				if (datNameTarget.equals(datName)
						|| lowDatNameTarget.equals(datName)) {
					// found
					continue LOOP_JSON;
				}
			}
			// not found
			deleteCacheFile(json);
			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((float) cacheSize / (1024.0f * 1024.0f))
					.append("MB(").append(cacheSize)
					.append(") freeSize=").append((float) freeSize / (1024.0f * 1024.0f))
					.append("MB(").append(freeSize)
					.append(") totalCachedFileSize=").append((float) totalCachedFileSize / (1024.0f * 1024.0f))
					.append("MB(").append(totalCachedFileSize).append(")")
					.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 {
					JSONObject obj = jsons.get(i);
					if (obj == null) {
						// 読み込み時に失敗→壊れたファイルなので最優先で消す
						oldestPos = i;
						oldestJson = filesJson.get(oldestPos);
						break;
					}
					lastPlayed = obj.getLong(KEY_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 datNameTarget = oldestJson.getName().replace(".json", ".dat");
			String lowDatNameTarget = oldestJson.getName().replace(".json", lowDatSuffix);
			boolean wasDatDeleted = false;
			boolean wasLowDatDeleted = false;
			for (Iterator<File> it = filesDat.iterator(); it.hasNext(); ) {
				File dat = it.next();
				String datName = dat.getName();
				boolean isEqualDatName = datNameTarget.equals(datName);
				boolean isEqualLowDatName = lowDatNameTarget.equals(datName);
				if (isEqualDatName || isEqualLowDatName) {
					totalCachedFileSize -= dat.length();
					deleteCacheFile(dat);
					it.remove();
					if (isEqualDatName) {
						wasDatDeleted = true;
					}
					if (isEqualLowDatName) {
						wasLowDatDeleted = true;
					}
					if (wasDatDeleted && wasLowDatDeleted) {
						break;
					}
				}
			}

			totalCachedFileSize -= oldestJson.length();
			deleteCacheFile(oldestJson);
			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) {
			deleteCacheFile(f);
		}
	}

	public static void deleteCacheFile(String videoNumber) {
		File json = new File(createCacheInfoPath(videoNumber));
		if (json.exists()) {
			deleteCacheFile(json);
		}
		File dat = new File(createCacheFilePath(videoNumber));
		if (dat.exists()) {
			deleteCacheFile(dat);
		}
		File lowDat = new File(createLowCacheFilePath(videoNumber));
		if (lowDat.exists()) {
			deleteCacheFile(lowDat);
		}
	}

	private static boolean deleteCacheFile(File file) {
		boolean ret = file.delete();
		if (ret) {
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf().append("Delete cache: ")
						.append(file.getAbsolutePath()).toString());
			}
		} else {
			Log.w(LOG_TAG, Log.buf().append("Delete failed: ")
					.append(file.getAbsolutePath()).toString());
		}
		return ret;
	}

	public static ArrayList<ExternalInfoData> getCacheExternalInfoData() {
        File dir = new File(STREAM_TEMP_DIR);
        File[] filesJsonArray = dir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String filename) {
                return filename.endsWith(".json");
            }
        });

        if (filesJsonArray == null) {
            // １個もキャッシュがないときは空のリストを返す
            return new ArrayList<ExternalInfoData>();
        }

        ArrayList<ExternalInfoData> datas = new ArrayList<ExternalInfoData>(
                filesJsonArray.length);
        for (File json : filesJsonArray) {
            ExternalInfoData eid = readExternalInfo(json);
            if (eid != null) {
                datas.add(eid);
            }
        }
        Collections.sort(datas, new Comparator<ExternalInfoData>() {
            @Override
            public int compare(ExternalInfoData object1, ExternalInfoData object2) {
                if (object2.lastPlayed > object1.lastPlayed) {
                    return 1;
                } else if (object2.lastPlayed == object1.lastPlayed) {
                    return 0;
                } else {
                    return -1;
                }
            }
        });
	    return datas;
	}

	@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) {
	    if (mIsFinish) {
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "loadMain(): already finished");
            }
	        return;
	    }
		HttpUriRequest httpRequest = createRequest();
		mHttpRequest = httpRequest;
		// 割り込み考慮して二重チェック
        if (mIsFinish) {
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "loadMain(): already finished");
            }
            return;
        }
		if (DEBUG_LOGD) {
			Log.d(LOG_TAG, "==========VideoLoader run httpRequest==========");
			Util.logHeaders(LOG_TAG, httpRequest.getAllHeaders());
			Log.d(LOG_TAG, "==========httpRequest end==========");
		}

		mHttpClient.getCookieStore().clear();
		InputStream inDownload = null;
		assert mTempFile == null;
		mTempFile = null;

		String lastModified = null;
		String etag = null;
		boolean wasSaveInfo = false;
		try {
			HttpResponse httpResponse = mHttpClient.execute(
					httpRequest
					);
			final StatusLine statusLine = httpResponse.getStatusLine();
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, "==========VideoLoader run httpResponse==========");
				Log.d(LOG_TAG, BasicLineFormatter.formatStatusLine(statusLine, null));
				Util.logHeaders(LOG_TAG, httpResponse.getAllHeaders());
				Log.d(LOG_TAG, "==========httpResponse end==========");
			}
			String lastCacheLength;
			String lastContentLength;
			String ifModifiedSince;
			String ifRange;
			if (mIsLow) {
				lastCacheLength = mTempInfoData.lastCacheLengthLow;
				lastContentLength = mTempInfoData.lastContentLengthLow;
				ifModifiedSince = mTempInfoData.ifModifiedSinceLow;
				ifRange = mTempInfoData.ifRangeLow;
			} else {
				lastCacheLength = mTempInfoData.lastCacheLength;
				lastContentLength = mTempInfoData.lastContentLength;
				ifModifiedSince = mTempInfoData.ifModifiedSince;
				ifRange = mTempInfoData.ifRange;
			}
			boolean startDownload;
			MultiRandomAccessFile tempFile;
			final int httpStatusCode = statusLine.getStatusCode();
			switch (httpStatusCode) {
			case HttpStatus.SC_NOT_MODIFIED: {
				// 更新無し、現在のファイルそのまま使用
				final long seekOffsetWrite = Long.parseLong(lastCacheLength);
				mContentLength = Long.parseLong(lastContentLength);
				assert seekOffsetWrite == mContentLength;
				tempFile = new MultiRandomAccessFile(mTempFilePath, false);
				mTempFile = tempFile;
				tempFile.setLength(mContentLength);
				tempFile.seekWrite(seekOffsetWrite);	// 一応必要なはず
				lastModified = ifModifiedSince;
				etag = ifRange;
				mLastModified = lastModified;
				mETag = etag;
				mIsCacheCompleted = true;
				startDownload = false;

				if (mEventListener != null) {
					if (!mIsCachedCalled) {
						mEventListener.onCached(this);
						mIsCachedCalled = true;
					}
					// 完了コールバックの前に情報保存
					if (lastModified != null /*&& etag != null*/) {
						saveInfo(lastModified, etag);
						wasSaveInfo = true;
					}
					mEventListener.onFinished(this);
				}
			} break;
			case HttpStatus.SC_PARTIAL_CONTENT: {
				// 続きから取得
				final long seekOffsetWrite = Long.parseLong(lastCacheLength);
				mContentLength = Long.parseLong(lastContentLength);
				assert seekOffsetWrite < mContentLength;
				tempFile = new MultiRandomAccessFile(mTempFilePath, true);
				mTempFile = tempFile;
				tempFile.setLength(mContentLength);
				tempFile.seekWrite(seekOffsetWrite);
				lastModified = Util.getFirstHeaderValue(httpResponse, HEADER_LAST_MODIFIED);
				etag = Util.getFirstHeaderValue(httpResponse, HEADER_ETAG);
				mLastModified = lastModified;
				mETag = etag;
				startDownload = true;
			} break;
			case HttpStatus.SC_OK: {
				// 最初から取得
				mContentLength = Long.parseLong(
						Util.getFirstHeaderValue(httpResponse, HEADER_CONTENT_LENGTH));
				if (DEBUG_LOGD) {
					Log.d(LOG_TAG, Log.buf().append("mContentLength=")
							.append(mContentLength).toString());
				}
				tempFile = new MultiRandomAccessFile(mTempFilePath, true);
				mTempFile = tempFile;
				// 事前にファイルサイズ変更
				tempFile.setLength(mContentLength);
				lastModified = Util.getFirstHeaderValue(httpResponse, HEADER_LAST_MODIFIED);
				etag = Util.getFirstHeaderValue(httpResponse, HEADER_ETAG);
				mLastModified = lastModified;
				mETag = etag;
				startDownload = true;
			} break;
			case HttpStatus.SC_FORBIDDEN: {
				// Cookie取り直してもう１回再取得を試みる
				if (tryRetry) {
					mHttpClient.getCookieStore().clear();
					mCookie = NicoroAPIManager.getCookieNicoHistory(
							mHttpClient, mVideoNumber, mCookieUserSession,
							mIsLow, mUserAgent);
					// ローカル変数のメモリ解放
					httpRequest = null;
					httpResponse = null;
					lastCacheLength = null;
					lastContentLength = null;
					ifModifiedSince = null;
					ifRange = null;

					loadMain(false);
					return;
				} else {
					// エラー
				    notifyHttpErrorToListener(statusLine);
					startDownload = false;
				}
			} break;
			case HttpStatus.SC_GATEWAY_TIMEOUT: {
			    // Gatewayのタイムアウト：サーバー不調時に発生
			    // とりあえずもう一回アクセスを試みる
                if (tryRetry) {
                    // ローカル変数のメモリ解放
                    httpRequest = null;
                    httpResponse = null;
                    lastCacheLength = null;
                    lastContentLength = null;
                    ifModifiedSince = null;
                    ifRange = null;

                    loadMain(false);
                    return;
                } else {
                    // エラー
                    notifyHttpErrorToListener(statusLine);
                    startDownload = false;
                }
			} break;
			default: {
				// エラー
                notifyHttpErrorToListener(statusLine);
				startDownload = false;
			} break;
			}

			if (startDownload) {
				if (mEventListener != null) {
					mEventListener.onStarted(this);
				}
				// 読み込み
				HttpEntity httpEntity = httpResponse.getEntity();
				inDownload = httpEntity.getContent();
				byte[] buffer = new byte[DOWNLOAD_BUFFER_SIZE];
				int offset = 0;
				while (!mIsFinish) {
					tempFile = mTempFile;
					// bufferは排他制御不要
					int readLength = buffer.length - offset;
					int read = inDownload.read(buffer, offset, readLength);
					if (read < 0) {
						if (offset > 0) {
							if (tempFile == null) {
								assert mIsFinish;
								break;
							}
							tempFile.write(buffer, 0, offset);
							offset = 0;
//							Log.d(LOG_TAG, "buffer read=" + read + " seekOffsetWrite=" + tempFile.tellWrite());
						}
						break;
					}
					offset += read;
					if (offset < buffer.length * 3 / 4) {
						continue;
					}
					if (tempFile == null) {
						assert mIsFinish;
						break;
					}
					tempFile.write(buffer, 0, offset);
					offset = 0;
					final long seekOffsetWrite = tempFile.tellWrite();
//					Log.d(LOG_TAG, "buffer read=" + read + " seekOffsetWrite=" + seekOffsetWrite);
					if (seekOffsetWrite >= mLastSaveInfoSize + SAVE_INFO_INTERVAL_SIZE) {
						// 一定サイズごとに情報保存（強制終了に備えて）
						// 時間間隔が短すぎてもパフォーマンス落ちるので時間も見る
						long currentTime = SystemClock.uptimeMillis();
						if (currentTime - mLastSaveInfoTime >= SAVE_INFO_INTERVAL_TIME_MS) {
							saveInfo(lastModified, etag);
							mLastSaveInfoSize = seekOffsetWrite;
							mLastSaveInfoTime = currentTime;
						}
					}
					if (mEventListener != null) {
						mEventListener.onNotifyProgress((int) seekOffsetWrite, (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");
				}
				tempFile = mTempFile;
				if (tempFile != null) {
					try {
						tempFile.syncWrite();
					} catch (IOException e) {
						Log.d(LOG_TAG, e.toString(), e);
					}
					tempFile.endWrite();
					final long seekOffsetWrite = tempFile.tellWrite();
					if (seekOffsetWrite == mContentLength) {
						mIsCacheCompleted = true;
						if (mEventListener != null) {
							// 読み込み完了でもキャッシュコールバック
							if (!mIsCachedCalled) {
								mEventListener.onCached(this);
								mIsCachedCalled = true;
							}
							// 完了コールバックの前に情報保存
							if (lastModified != null /*&& etag != null*/) {
								saveInfo(lastModified, etag);
								wasSaveInfo = true;
							}
							mEventListener.onFinished(this);
						}
					}
				}
			}
		} catch (ClientProtocolException e) {
			String errorMessage = e.toString();
			assert errorMessage != null;
			if (mEventListener != null) {
				mEventListener.onOccurredError(this, errorMessage);
			}
			Log.d(LOG_TAG, errorMessage, e);
		} catch (IOException e) {
			String errorMessage = e.toString();
			assert errorMessage != null;
			if (mEventListener != null) {
				mEventListener.onOccurredError(this, errorMessage);
			}
			Log.d(LOG_TAG, errorMessage, e);
		} finally {
			if (inDownload != null) {
				try {
					inDownload.close();
				} catch (IOException e) {
					Log.d(LOG_TAG, e.toString(), e);
				}
			}

			// 情報保存
			if (lastModified != null /*&& etag != null*/ && !wasSaveInfo) {
				saveInfo(lastModified, etag);
			}
		}
	}

	private void readInfo() {
	    readInfo(mTempInfoPath, mTempInfoData);
	}

	private void readInfo(String path, InfoData infoData) {
		JSONObject jsonLoad = Util.createJSONFromFile(path);
		infoData.reset();
		if (jsonLoad == null) {
			// ない場合はそのまま処理継続
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf().append(path)
						.append(" load failed, continue.").toString());
			}
		} else {
			if (DEBUG_LOGD) {
				Log.d(LOG_TAG, Log.buf().append("Load ").append(path)
						.append(": ").append(jsonLoad.toString()).toString());
			}
			infoData.ifModifiedSince = jsonLoad.optString(HEADER_LAST_MODIFIED, null);
			infoData.ifRange = jsonLoad.optString(HEADER_ETAG, null);
			infoData.lastContentLength = jsonLoad.optString(HEADER_CONTENT_LENGTH, null);
			infoData.lastCacheLength = jsonLoad.optString(KEY_CACHED_LENGTH, null);

			infoData.ifModifiedSinceLow = jsonLoad.optString(HEADER_LAST_MODIFIED + LOW_VIDEO_SUFFIX, null);
			infoData.ifRangeLow = jsonLoad.optString(HEADER_ETAG + LOW_VIDEO_SUFFIX, null);
			infoData.lastContentLengthLow = jsonLoad.optString(HEADER_CONTENT_LENGTH + LOW_VIDEO_SUFFIX, null);
			infoData.lastCacheLengthLow = jsonLoad.optString(KEY_CACHED_LENGTH + LOW_VIDEO_SUFFIX, null);
		}
	}

	private void saveInfo(String lastModified, String etag) {
		assert lastModified != null;
//		assert etag != null;
		assert mContentLength > 0;
		MultiRandomAccessFile tempFile = mTempFile;
		if (tempFile == null) {
//			assert false;
			Log.e(LOG_TAG, "saveInfo failed: mTempFile is null");
			return;
		}
		final long seekOffsetWrite = tempFile.tellWrite();
		assert seekOffsetWrite >= 0;

		try {
			JSONObject jsonSave = new JSONObject();
			if (mIsLow) {
				jsonSave
					.put(HEADER_CONTENT_LENGTH,
							mTempInfoData.lastContentLength)
					.put(HEADER_LAST_MODIFIED,
							mTempInfoData.ifModifiedSince)
					.put(HEADER_ETAG,
							mTempInfoData.ifRange)
					.put(KEY_CACHED_LENGTH,
							mTempInfoData.lastCacheLength);
				jsonSave
					.put(HEADER_CONTENT_LENGTH + LOW_VIDEO_SUFFIX,
							mContentLength)
					.put(HEADER_LAST_MODIFIED + LOW_VIDEO_SUFFIX,
							lastModified)
					.put(HEADER_ETAG + LOW_VIDEO_SUFFIX,
							etag)
					.put(KEY_CACHED_LENGTH + LOW_VIDEO_SUFFIX,
							seekOffsetWrite);
			} else {
				jsonSave
					.put(HEADER_CONTENT_LENGTH,
							mContentLength)
					.put(HEADER_LAST_MODIFIED,
							lastModified)
					.put(HEADER_ETAG,
							etag)
					.put(KEY_CACHED_LENGTH,
							seekOffsetWrite);
				jsonSave
					.put(HEADER_CONTENT_LENGTH + LOW_VIDEO_SUFFIX,
							mTempInfoData.lastContentLengthLow)
					.put(HEADER_LAST_MODIFIED + LOW_VIDEO_SUFFIX,
							mTempInfoData.ifModifiedSinceLow)
					.put(HEADER_ETAG + LOW_VIDEO_SUFFIX,
							mTempInfoData.ifRangeLow)
					.put(KEY_CACHED_LENGTH + LOW_VIDEO_SUFFIX,
							mTempInfoData.lastCacheLengthLow);
			}
			jsonSave.put(KEY_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.toString(), e);
		}
	}

    private static ExternalInfoData readExternalInfo(File file) {
        JSONObject jsonLoad = Util.createJSONFromFile(file);
        if (jsonLoad == null) {
            return null;
        }
        ExternalInfoData data = new ExternalInfoData();
        data.lastPlayed = jsonLoad.optLong(KEY_LAST_PLAYED, 0L);
        String fileName = file.getName();
        int end = fileName.indexOf(".json");
        assert end > 0;
        data.videoNumber = fileName.substring(0, end);
        return data;
    }

	@Override
	public int readFromNativeCallback(int bufSize, byte[] buffer) {
	    try {
    		MultiRandomAccessFile tempFile = mTempFile;
    		int ret;
    		if (tempFile == null) {
    			ret = -1;
    		} else {
    			try {
    				if (DEBUG_LOGD_READ) {
    					Log.d(LOG_TAG, Log.buf()
    							.append("readFromNativeCallback: bufSize=").append(bufSize)
    							.append(" buffer=").append(buffer.toString())
    							.append(" mTempFile.tellRead()=").append(tempFile.tellRead())
    							.append(" mTempFile.tellWrite()=").append(tempFile.tellWrite())
    							.toString());
    				}
    				if (tempFile.needWaitToRead()) {
    					// フリーズ防止のためすぐ戻す
    					ret = 0;
    				} else {
    				    if (bufSize > buffer.length) {
    				        bufSize = buffer.length;
    				    }
    					ret = tempFile.read(buffer, 0, bufSize);
    				}
    			} catch (IOException e) {
    				Log.d(LOG_TAG, e.toString(), e);
    				ret = -1;
    			}
    		}
    		if (DEBUG_LOGD_READ) {
    			Log.d(LOG_TAG, Log.buf().append("readFromNativeCallback: return=")
    					.append(ret).toString());
    		}
    		return ret;
        } catch (Throwable e) {
            Log.e(LOG_TAG, e.toString(), e);
            return -1;
        }
	}

	private HttpUriRequest createRequest() {
		HttpUriRequest httpRequest = new HttpGet(mUrl);
		httpRequest.addHeader("Cookie", mCookie);
		if (mUserAgent != null) {
			httpRequest.setHeader("User-Agent", mUserAgent);
		}

		String lastContentLength;
		String lastCacheLength;
		String ifModifiedSince;
		String ifRange;
		if (mIsLow) {
			lastContentLength = mTempInfoData.lastContentLengthLow;
			lastCacheLength = mTempInfoData.lastCacheLengthLow;
			ifModifiedSince = mTempInfoData.ifModifiedSinceLow;
			ifRange = mTempInfoData.ifRangeLow;
		} else {
			lastContentLength = mTempInfoData.lastContentLength;
			lastCacheLength = mTempInfoData.lastCacheLength;
			ifModifiedSince = mTempInfoData.ifModifiedSince;
			ifRange = mTempInfoData.ifRange;
		}

		if (lastContentLength != null
				&& lastContentLength.equals(lastCacheLength)) {
			// 更新チェックを試みる
			if (ifModifiedSince != null) {
				httpRequest.addHeader("If-Modified-Since", ifModifiedSince);
			}
		} else {
			if (ifRange != null && lastCacheLength != null) {
				// 継続ダウンロードを試みる
				httpRequest.addHeader("If-Range", ifRange);
				httpRequest.addHeader("Range", "bytes= " + 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;
		}
		String lastContentLength = jsonLoad.optString(
				HEADER_CONTENT_LENGTH, null);
		String lastCacheLength = jsonLoad.optString(
				KEY_CACHED_LENGTH, null);
		if (lastContentLength != null && lastContentLength.equals(lastCacheLength)) {
			// TODO とりあえず更新チェック省略
			return true;
		} else {
			return false;
		}
	}

	public static final boolean isFinishedCacheLow(String videoNumber) {
		String tempInfoPath = createCacheInfoPath(videoNumber);
		JSONObject jsonLoad = Util.createJSONFromFile(tempInfoPath);
		if (jsonLoad == null) {
			return false;
		}
		String lastContentLength = jsonLoad.optString(
				HEADER_CONTENT_LENGTH + LOW_VIDEO_SUFFIX, null);
		String lastCacheLength = jsonLoad.optString(
				KEY_CACHED_LENGTH + LOW_VIDEO_SUFFIX, null);
		if (lastContentLength != null && lastContentLength.equals(lastCacheLength)) {
			// TODO とりあえず更新チェック省略
			return true;
		} else {
			return false;
		}
	}

	public static final boolean hasAnyCacheFile(String videoNumber) {
		File json = new File(createCacheInfoPath(videoNumber));
		if (json.exists()) {
			return true;
		}
		File dat = new File(createCacheFilePath(videoNumber));
		if (dat.exists()) {
			return true;
		}
		File lowDat = new File(createLowCacheFilePath(videoNumber));
		if (lowDat.exists()) {
			return true;
		}
		return false;
	}

	public boolean isLow() {
		return mIsLow;
	}

	public String getMovieType() {
		if (mMovieType == null) {
			MultiRandomAccessFile tempFile = mTempFile;
			if (tempFile == null) {
				assert false;
				return "(unknown)";
			} else {
				// ファイルの先頭読み込んで判定
				byte[] head = new byte[3];
				try {
					int read;
					synchronized (tempFile.getSync()) {
					    final long orgRead = tempFile.tellRead();
						tempFile.seekRead(0);
						read = tempFile.read(head);
						tempFile.seekRead(orgRead);
					}
					if (read != head.length) {
						return "(unknown)";
					}
				} catch (IOException e) {
					Log.d(LOG_TAG, e.toString(), e);
					return "(unknown)";
				}
				if ((head[0] & 0xff) == 'F'
					&& (head[1] & 0xff) == 'L'
						&& (head[2] & 0xff) == 'V') {
					mMovieType = "flv";
				} else {
					// その他はmp4扱い
					mMovieType = "mp4";
				}
			}
		}
		return mMovieType;
	}

	public String getContentType() {
		return "video/" + getMovieType();
	}

	public boolean isCacheCompleted() {
		return mIsCacheCompleted;
	}

	private void notifyHttpErrorToListener(StatusLine statusLine) {
        if (mEventListener != null) {
            String errorMessage = BasicLineFormatter.formatStatusLine(
                    statusLine, null);
            mEventListener.onOccurredError(this, errorMessage);
        }
	}

	static final int SEEK_SET = 0;
	static final int SEEK_CUR = 1;
	static final int SEEK_END = 2;
	static final int AVSEEK_SIZE = 0x10000;

	@Override
	public long seekFromNativeCallback(long offset, int whence) {
	    try {
    		MultiRandomAccessFile tempFile = mTempFile;
    		if (DEBUG_LOGD_SEEK) {
    			StringBuilder buf = Log.buf()
    				.append("seekFromNativeCallback: offset=").append(offset)
    				.append(" whence=").append(whence);
    			if (tempFile == null) {
    				buf.append(" mTempFile is null!");
    			} else {
    				buf.append(" mTempFile.tellRead()=").append(mTempFile.tellRead());
    			}
    			Log.d(LOG_TAG, buf.toString());
    		}
    		long ret = -1L;
    		try {
    			switch (whence) {
    			case SEEK_SET:
    				if (tempFile != null) {
    					ret = tempFile.seekRead(offset, MultiRandomAccessFile.SEEK_SET);
    				}
    				break;
    			case SEEK_CUR:
    				if (tempFile != null) {
    					ret = tempFile.seekRead(offset, MultiRandomAccessFile.SEEK_CUR);
    				}
    				break;
    			case SEEK_END:
    				if (tempFile != null) {
    					ret = tempFile.seekRead(offset, MultiRandomAccessFile.SEEK_END);
    				}
    				break;
    			case AVSEEK_SIZE:
//    				if (tempFile != null) {
//    					ret = tempFile.length();
//    				}
    				ret = -1;
    				break;
    			}
    		} catch (IOException e) {
    			Log.e(LOG_TAG, e.toString(), e);
    			assert ret == -1;
    		}

    		if (DEBUG_LOGD_SEEK) {
    			Log.d(LOG_TAG, Log.buf().append("seekFromNativeCallback: return=").append(ret).toString());
    		}
    		return ret;
        } catch (Throwable e) {
            Log.e(LOG_TAG, e.toString(), e);
            return -1;
        }
	}

	public interface EventListener {
		/**
		 * キャッシュのダウンロードを開始した（＝ダウンロードの必要があった）
		 * @param streamLoader
		 */
		public void onStarted(VideoLoader streamLoader);
		/**
		 * ファイルの先頭部分の読み込み完了
		 * @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);
	}

	public InputStream createInputStream() {
		MultiRandomAccessFile tempFile = mTempFile;
		if (tempFile == null) {
			return null;
		} else {
			return tempFile.createInputStream();
		}
	}
}
