package jp.sourceforge.nicoro;

import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;

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 static jp.sourceforge.nicoro.NicoroAPIManager.ECO_TYPE_HIGH;
import static jp.sourceforge.nicoro.NicoroAPIManager.ECO_TYPE_MID;
import static jp.sourceforge.nicoro.NicoroAPIManager.ECO_TYPE_LOW;
import jp.sourceforge.nicoro.R;
import jp.gr.java_conf.shiseissi.commonlib.APILevelWrapper;
import jp.gr.java_conf.shiseissi.commonlib.FileUtil;

import android.content.Context;
import android.content.SharedPreferences;
import android.net.wifi.WifiManager;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
//import android.os.Parcel;
//import android.os.Parcelable;
import android.os.SystemClock;
import android.text.TextUtils;


public class VideoLoader implements Runnable, FFmpegIOCallback,
Handler.Callback, VideoLoaderInterface {
    private static final boolean DEBUG_LOGV = Release.IS_DEBUG & true;
	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;

	private static final String STREAM_TEMP_DEVICE_ROOT =
		Environment.getExternalStorageDirectory().getAbsolutePath();

    public static final String STREAM_TEMP_DIR_OLD_VERSION =
        STREAM_TEMP_DEVICE_ROOT + "/NicoRo/";

    private static final String EXT_DAT = ".dat";
    private static final String EXT_JSON = ".json";

	/**
	 * キャッシュに必要な最小限の空きサイズ
	 *
	 * 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 SAVE_INFO_INTERVAL_TIME_MS = 10 * 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 int DOWNLOAD_BUFFER_SIZE = 256 * 1024;

	private static final int THREAD_PRIORITY_WRITE =
//	    android.os.Process.THREAD_PRIORITY_FOREGROUND;
        android.os.Process.THREAD_PRIORITY_DEFAULT;

	private static final int CONNECTION_TIMEOUT_MS = 60 * 1000;
    private static final int SO_TIMEOUT_MS = 60 * 1000;

    private static final long WAIT_FINISH_WITHOUT_SHUTDOWN_MS = 3000L;

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

	private static final int MSG_ID_SHOW_INFO_TOAST = 0;

	private static final int FORBIDDEN_RETRY_COUNT = 3;
    private static final int GATEWAY_TIMEOUT_RETRY_COUNT = 3;
    private static final int NETWORK_IO_RETRY_COUNT = 10;

    private static final int WAIT_FOR_NETWORK_ERROR = 3000;

	private static class DatFileFilter implements FilenameFilter {
        @Override
        public boolean accept(File dir, String filename) {
            return filename.endsWith(EXT_DAT);
        }
	}

    private static class JsonFileFilter implements FilenameFilter {
        @Override
        public boolean accept(File dir, String filename) {
            return filename.endsWith(EXT_JSON);
        }
    }

    private static class DatJsonFileFilter implements FilenameFilter {
        @Override
        public boolean accept(File dir, String filename) {
            return filename.endsWith(EXT_DAT)
                || filename.endsWith(EXT_JSON);
        }
    }

	private DefaultHttpClient mHttpClient;
	private String mUrl;
	private String mCookie;
	private String mVideoV;
	private Context mContext;
	private String mCookieUserSession;
    private int mThreadPriority;
    private boolean mIsPriorityLowerThanWrite;
    private int mEcoType;
//	private int mBufferOffset = 0;
	private volatile boolean mIsFinish = false;
	private final Object mSync = new Object();
	private Thread mThread = new Thread(this, "VideoLoader");
	private long mContentLength = -1;
	private String mETag;
	private String mLastModified;
	private boolean mIsStarted = false;

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

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

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

	private String mTempFilePath;
	private String mTempInfoPath;

    private AtomicReference<InputStream> mInDownload =
        new AtomicReference<InputStream>();

    private final HandlerWrapper mHandler = new HandlerWrapper(this, Looper.getMainLooper());

    @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_ID_SHOW_INFO_TOAST:
                String info = (String) msg.obj;
                Util.showInfoToast(mContext, info);
                break;
            default:
                assert false : msg.what;
                break;
        }
        return true;
    }

    // FIXME ViewPagerのFragment復帰でどうしてもクラッシュすることがあるので、いったん実装を変える
	public static class ExternalInfoData implements /*Parcelable,*/ Serializable {
        /**
         *
         */
        private static final long serialVersionUID = 1969670228257854777L;

        public String videoV;
        public long lastPlayed;

        public ExternalInfoData() {
        }

//        private ExternalInfoData(Parcel in) {
//            videoV = in.readString();
//            lastPlayed = in.readLong();
//        }
//
//        @Override
//        public int describeContents() {
//            return 0;
//        }
//
//        @Override
//        public void writeToParcel(Parcel dest, int flags) {
//            dest.writeString(videoV);
//            dest.writeLong(lastPlayed);
//        }
//
//        public static final Parcelable.Creator<ExternalInfoData> CREATOR = new Parcelable.Creator<ExternalInfoData>() {
//            @Override
//            public ExternalInfoData createFromParcel(Parcel source) {
//                return new ExternalInfoData(source);
//            }
//
//            @Override
//            public ExternalInfoData[] newArray(int size) {
//                return new ExternalInfoData[size];
//            }
//
//        };
	}

    static class InfoDataPart {
        String ifModifiedSince;
        String ifRange;
        String lastContentLength;
        String lastCacheLength;

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

    static class InfoData {
        InfoDataPart high = new InfoDataPart();
        InfoDataPart mid = new InfoDataPart();
        InfoDataPart low = new InfoDataPart();

        public void reset() {
            high.reset();
            mid.reset();
            low.reset();
        }
    }
	private InfoData mTempInfoData = new InfoData();

	private long mCacheSize;
	private String mUserAgent;

	private String mMovieType;

//	private HttpUriRequest mHttpRequest;

	private int mForbiddenRetryCount;
	private int mGatewayTimeoutRetryCount;
	private int mNetworkIORetryCount;

	private boolean mHasStarted;

	public VideoLoader(String url, String cookie, String videoV,
			Context context, String cookieUserSession, int threadPriority) {
		mHttpClient = Util.createHttpClient(CONNECTION_TIMEOUT_MS, SO_TIMEOUT_MS);
		mUrl = url;
		mCookie = cookie;
		mVideoV = videoV;
		mContext = context;
		mCookieUserSession = cookieUserSession;
		mThreadPriority = threadPriority;
		mIsPriorityLowerThanWrite = threadPriority >= THREAD_PRIORITY_WRITE;
		if (NicoroAPIManager.isGetflvUrlLow(url)) {
		    mEcoType = ECO_TYPE_LOW;
		} else if (NicoroAPIManager.isGetflvUrlMid(url)) {
            mEcoType = ECO_TYPE_MID;
		} else {
            mEcoType = ECO_TYPE_HIGH;
		}

		File dir = new File(getStreamTempDir());
		dir.mkdirs();
//		mTempFilePath = createCacheFilePath(url);
		if (mEcoType == ECO_TYPE_LOW) {
			mTempFilePath = createLowCacheFilePath(context, videoV);
		} else if (mEcoType == ECO_TYPE_MID) {
            mTempFilePath = createMidCacheFilePath(context, videoV);
		} else {
			mTempFilePath = createCacheFilePath(context, videoV);
		}
		mTempInfoPath = createCacheInfoPath(context, videoV);

		SharedPreferences sharedPreference = Util.getDefaultSharedPreferencesMultiProcess(mContext);
		mCacheSize = getCacheSizeSettings(mContext, sharedPreference);
		mUserAgent = sharedPreference.getString(NicoroConfig.USER_AGENT, null);
	}

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

    @Override
	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();
	}

    @Override
	public void finish() {
        setFinish();

        try {
            // まず強制終了なしで待機してみる
            mThread.join(WAIT_FINISH_WITHOUT_SHUTDOWN_MS);
        } catch (InterruptedException e) {
            Log.d(LOG_TAG, e.toString(), e);
        }
        if (mThread.isAlive()) {
            mHttpClient.getConnectionManager().shutdown();
//            Util.abortHttpUriRequest(mHttpRequest);
//            InputStream inDownload = mInDownload.getAndSet(null);
//            if (inDownload != null) {
//                if (DEBUG_LOGD) {
//                    Log.d(LOG_TAG, "VideoLoader close start at finish");
//                }
//                FileUtil.closeIgnoreException(inDownload);
//                if (DEBUG_LOGD) {
//                    Log.d(LOG_TAG, "VideoLoader close end at finish");
//                }
//            }
            while (true) {
                try {
                    // ファイル書き込みで数秒以上フリーズする場合がある？のでいっそタイムアウトなしに
                    mThread.join();
                    break;
                } catch (InterruptedException e) {
                    Log.d(LOG_TAG, e.toString(), e);
                }
            }
        }

		mIsStarted = false;

		AbstractMultiRandomAccessFile tempFile = mTempFile;
		if (tempFile != null) {
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "VideoLoader mTempFile close start at finish");
            }
			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);
			}
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "VideoLoader mTempFile close start at end");
            }
		}
	}

    @Override
	public void finishAsync(ExecutorService executorService,
	        final CallbackMessage<Void, Void> callback) {
	    if (mIsFinish) {
	        if (callback != null) {
	            callback.sendMessageSuccess(null);
	        }
	    } else {
	        setFinish();
	        executorService.execute(new Runnable() {
                @Override
                public void run() {
                    finish();
                    if (callback != null) {
                        callback.sendMessageSuccess(null);
                    }
                }
            });
	    }
	}

    @Override
    public void finishAsync(ExecutorService executorService,
            final CountDownLatch latch) {
        if (mIsFinish) {
            latch.countDown();
        } else {
            setFinish();
            // Mainスレッドから呼ばれるとは限らないので、
            // AsyncTaskではなくExecutorServiceを使う
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    finish();
                    latch.countDown();
                }
            });
        }
    }

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

    @Override
	public void setEventListener(VideoLoaderInterface.EventListener eventListener) {
		mEventListener = eventListener;
	}

    @Override
	public String getFilePath() {
		return mTempFilePath;
	}

    @Override
	public String getVideoV() {
		return mVideoV;
	}

    @Override
	public long getContentLength() {
		return mContentLength;
	}

    @Override
	public String getETag() {
		return mETag;
	}

    @Override
	public String getLastModified() {
		return mLastModified;
	}

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

	private static String createCacheFilePath(Context context, String videoV) {
		return new File(getStreamTempDir(context),
		        videoV + EXT_DAT).getAbsolutePath();
	}
    private static String createMidCacheFilePath(Context context, String videoV) {
        return new File(getStreamTempDir(context),
                videoV + MID_VIDEO_SUFFIX + EXT_DAT).getAbsolutePath();
    }
	private static String createLowCacheFilePath(Context context, String videoV) {
		return new File(getStreamTempDir(context),
		        videoV + LOW_VIDEO_SUFFIX + EXT_DAT).getAbsolutePath();
	}
	private static String createCacheInfoPath(Context context, String videoV) {
		return new File(getStreamTempDir(context),
		        videoV + EXT_JSON).getAbsolutePath();
	}

    /**
     * 生放送用のキャッシュファイルのパスを取得
     * @return
     */
    static String createLiveCacheFilePath(Context context) {
        return new File(getStreamTempDir(context),
                "livetemp.dat").getAbsolutePath();
    }

    public static boolean isStreamTempDirWritable(Context context) {
        String streamTempDir = getStreamTempDir(context);
        if (streamTempDir.startsWith(Environment.getExternalStorageDirectory()
                .getAbsolutePath())) {
            String state = Environment.getExternalStorageState();
            if (Environment.MEDIA_MOUNTED.equals(state)) {
                return true;
            }
        }
        return new File(streamTempDir).canWrite();
    }

    public static boolean isStreamTempDirReadable(Context context) {
        String streamTempDir = getStreamTempDir(context);
        if (streamTempDir.startsWith(Environment.getExternalStorageDirectory()
                .getAbsolutePath())) {
            String state = Environment.getExternalStorageState();
            if (Environment.MEDIA_MOUNTED.equals(state)
                    || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
                return true;
            }
        }
        return new File(streamTempDir).canRead();
    }

    /**
     * キャッシュファイルを削除
     * @param context
     * @param cacheSize 許可するキャッシュサイズ（バイト単位）
     * @param videoV これから使用する動画の番号
     * @return
     */
	private static Set<String> deleteCacheFile(Context context, long cacheSize,
	        String videoV) {
	    if (DEBUG_LOGV) {
	        Log.v(LOG_TAG, Log.buf().append("VideoLoader#deleteCacheFile: cacheSize=")
	                .append(cacheSize));
	    }

		// TODO: スレッド終了フラグが立ったときに中断しなくて大丈夫か？

	    HashSet<String> deleteNumbers = new HashSet<String>();

		// サイズ総計算
	    String streamTempDir = getStreamTempDir(context);
		File dir = new File(streamTempDir);
		LinkedList<File> filesDat = getDatFiles(dir);
		LinkedList<File> filesJson = getJsonFiles(dir);

		// 整合性取れていないファイルは最優先で消す
		final String lowDatSuffix = LOW_VIDEO_SUFFIX + EXT_DAT;
        final String midDatSuffix = MID_VIDEO_SUFFIX + EXT_DAT;
		LOOP_DAT : for (Iterator<File> it = filesDat.iterator(); it.hasNext(); ) {
			File dat = it.next();
			String jsonNameTarget = dat.getName()
				.replace(lowDatSuffix, EXT_JSON)
                .replace(midDatSuffix, EXT_JSON)
				.replace(EXT_DAT, EXT_JSON);
			for (File json : filesJson) {
				if (jsonNameTarget.equals(json.getName())) {
					// found
					continue LOOP_DAT;
				}
			}
			// not found
	        if (DEBUG_LOGV) {
	            Log.v(LOG_TAG, Log.buf().append(jsonNameTarget).append(" not found, delete ")
	                    .append(dat.getName()));
	        }
			deleteCacheFile(dat);
			deleteNumbers.add(jsonNameTarget.replace(EXT_JSON, ""));
			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(EXT_JSON, EXT_DAT);
			String lowDatNameTarget = jsonName.replace(EXT_JSON, lowDatSuffix);
            String midDatNameTarget = jsonName.replace(EXT_JSON, midDatSuffix);
			for (File dat : filesDat) {
				String datName = dat.getName();
				if (datNameTarget.equals(datName)
						|| lowDatNameTarget.equals(datName)
						|| midDatNameTarget.equals(datName)) {
					// found
					continue LOOP_JSON;
				}
			}
			// not found
            if (DEBUG_LOGV) {
                Log.v(LOG_TAG, Log.buf().append(datNameTarget).append(" and <low> <mid>m not found, delete ")
                        .append(jsonName));
            }
			deleteCacheFile(json);
            deleteNumbers.add(jsonName.replace(EXT_JSON, ""));
			it.remove();
		}

		long totalCachedFileSize = getTotalFileSize(filesDat);
		totalCachedFileSize += getTotalFileSize(filesJson);

        StorageInfo storageInfo = new StorageInfo(streamTempDir);
        long freeSize = storageInfo.getAvailableBytes();
        if (freeSize >= 0) {
    		// 空き容量に余裕がないときはさらに余分に消す
    		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(storageInfo.getAvailable())
    					.append("(").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;
    		}
        } else {
            // 空き容量取得失敗でもそのまま続行させる
		}

		if (totalCachedFileSize <= cacheSize) {
			// 指定キャッシュサイズ以下なら何もせず
			return deleteNumbers;
		}

		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) {
					    File targetJson = filesJson.get(i);
					    // これから使用する動画なら空き容量確保の削除からは除外
					    if (!targetJson.getName().replace(EXT_JSON, "").equals(videoV)) {
	                        oldestPos = i;
	                        oldestJson = targetJson;
	                        oldestTime = lastPlayed;
					    }
					}
				} catch (JSONException e) {
					// 壊れたファイルなので最優先で消す
					oldestPos = i;
					oldestJson = filesJson.get(oldestPos);
					break;
				}
			}
			if (oldestJson == null) {
			    // 削除対象無し
			    break;
			}

			String oldestNumber = oldestJson.getName().replace(EXT_JSON, "");
			String datNameTarget = oldestNumber + EXT_DAT;
			String lowDatNameTarget = oldestNumber + lowDatSuffix;
            String midDatNameTarget = oldestNumber + midDatSuffix;
			boolean wasDatDeleted = false;
			boolean wasLowDatDeleted = false;
            boolean wasMidDatDeleted = 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);
                boolean isEqualMidDatName = midDatNameTarget.equals(datName);
				if (isEqualDatName || isEqualLowDatName || isEqualMidDatName) {
					totalCachedFileSize -= dat.length();
					deleteCacheFile(dat);
					it.remove();
					if (isEqualDatName) {
						wasDatDeleted = true;
					}
					if (isEqualLowDatName) {
						wasLowDatDeleted = true;
					}
                    if (isEqualMidDatName) {
                        wasMidDatDeleted = true;
                    }
					if (wasDatDeleted && wasLowDatDeleted && wasMidDatDeleted) {
						break;
					}
				}
			}

			totalCachedFileSize -= oldestJson.length();
			deleteCacheFile(oldestJson);
            deleteNumbers.add(oldestNumber);
			filesJson.remove(oldestJson);
			jsons.remove(oldestPos);
		}

		return deleteNumbers;
	}

	public static void deleteAllCacheFile(Context context) {
		File[] filesArray = getAllCacheFiles(getStreamTempDir(context));
		if (filesArray == null) {
			// キャッシュファイル無し
			return;
		}
		for (File f : filesArray) {
			deleteCacheFile(f);
		}
	}

	public static void deleteCacheFile(Context context, String videoV) {
		File json = new File(createCacheInfoPath(context, videoV));
		if (json.exists()) {
			deleteCacheFile(json);
		}
		File dat = new File(createCacheFilePath(context, videoV));
		if (dat.exists()) {
			deleteCacheFile(dat);
		}
		File lowDat = new File(createLowCacheFilePath(context, videoV));
		if (lowDat.exists()) {
			deleteCacheFile(lowDat);
		}
        File midDat = new File(createMidCacheFilePath(context, videoV));
        if (midDat.exists()) {
            deleteCacheFile(midDat);
        }
	}

    public static void moveCacheFiles(String newCacheDirPath,
            String oldCacheDirPath) {
        File newCacheDir = new File(newCacheDirPath);
        File oldCacheDir = new File(oldCacheDirPath);

        File[] oldFiles = getAllCacheFiles(oldCacheDir);
        if (oldFiles == null) {
            // キャッシュファイル無し
            return;
        }

        for (File f : oldFiles) {
            FileUtil.moveFile(f, new File(newCacheDir, f.getName()));
        }
    }

	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 LinkedList<File> getDatFiles(String streamTempDir) {
        return getDatFiles(new File(streamTempDir));
	}
    public static LinkedList<File> getDatFiles(File dir) {
        File[] filesDatArray = dir.listFiles(new DatFileFilter());
        if (filesDatArray == null) {
            return new LinkedList<File>();
        } else {
            return new LinkedList<File>(Arrays.asList(filesDatArray));
        }
	}

    public static LinkedList<File> getJsonFiles(String streamTempDir) {
        return getJsonFiles(new File(streamTempDir));
    }
    public static LinkedList<File> getJsonFiles(File dir) {
        File[] filesJsonArray = dir.listFiles(new JsonFileFilter());
        if (filesJsonArray == null) {
            return new LinkedList<File>();
        } else {
            return new LinkedList<File>(Arrays.asList(filesJsonArray));
        }
    }

    public static File[] getAllCacheFiles(String streamTempDir) {
        return getAllCacheFiles(new File(streamTempDir));
    }
    public static File[] getAllCacheFiles(File dir) {
        return dir.listFiles(new DatJsonFileFilter());
    }

    public static long getTotalFileSize(Iterable<File> list) {
        long totalFileSize = 0;
        for (File f : list) {
            totalFileSize += f.length();
        }
        return totalFileSize;
    }

	public static ArrayList<ExternalInfoData> getCacheExternalInfoData(Context context) {
        File dir = new File(getStreamTempDir(context));
        File[] filesJsonArray = dir.listFiles(new JsonFileFilter());

        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() {
        android.os.Process.setThreadPriority(mThreadPriority);

		if (DEBUG_LOGD) {
			Log.d(LOG_TAG, "VideoLoader run");
		}

		// ストレージデバイス書き込み不可の時はエラー
		if (!isStreamTempDirWritable(mContext)) {
			if (mEventListener != null) {
				String errorMessage = mContext.getString(R.string.errormessage_player_strage_unmount);
				mEventListener.onOccurredError(this, errorMessage);
			}
			return;
		}

		// 先にキャッシュファイル削除
		Set<String> deleteNumbers = deleteCacheFile(mContext, mCacheSize, mVideoV);
		if (DEBUG_LOGD) {
		    Log.d(LOG_TAG, "Delete cache list:>>>>>");
		    for (String number : deleteNumbers) {
		        Log.d(LOG_TAG, number);
		    }
            Log.d(LOG_TAG, "<<<<<");
		}
		if (deleteNumbers.contains(mVideoV)) {
		    mHandler.obtainMessage(MSG_ID_SHOW_INFO_TOAST,
		            mContext.getResources().getString(
		                    R.string.toast_invalid_cache_deleted, mVideoV));
		}
		deleteNumbers = null;

        assert mTempFile == null;
        mTempFile = null;

		mForbiddenRetryCount = FORBIDDEN_RETRY_COUNT;
		mGatewayTimeoutRetryCount = GATEWAY_TIMEOUT_RETRY_COUNT;
		mNetworkIORetryCount = NETWORK_IO_RETRY_COUNT;

		while (!mIsFinish) {
	        if (DEBUG_LOGD) {
	            Log.d(LOG_TAG, Log.buf()
	                    .append("mForbiddenRetryCount=").append(mForbiddenRetryCount)
                        .append(" mGatewayTimeoutRetryCount=").append(mGatewayTimeoutRetryCount)
                        .append(" mNetworkIORetryCount=").append(mNetworkIORetryCount)
	                    .toString());
	        }
		    if (!loadMain()) {
		        break;
		    }
		    if (mIsFinish) {
		        break;
		    }
		    // いったん破棄されるので再生成
	        mHttpClient = Util.createHttpClient(CONNECTION_TIMEOUT_MS, SO_TIMEOUT_MS);
		}

		if (DEBUG_LOGD) {
			Log.d(LOG_TAG, "VideoLoader run end");
		}
	}

	/**
	 * キャッシュ処理本体（ネットワークからダウンロード＆ファイルに保存）
	 * @return trueならリトライの可能性、falseなら完全に終了
	 */
	private boolean loadMain() {
	    if (mIsFinish) {
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "loadMain(): already finished");
            }
	        return false;
	    }

        // 情報読み込み
        readInfo();
        // 割り込み考慮して二重チェック
        if (mIsFinish) {
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "loadMain(): already finished");
            }
            return false;
        }

		HttpUriRequest httpRequest = createRequest();
        if (mIsFinish) {
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "loadMain(): already finished");
            }
            return false;
        }
//        mHttpRequest = httpRequest;
		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;

		String lastModified = null;
		String etag = null;
		boolean wasSaveInfo = false;
		HttpEntity httpEntity = null;
		boolean fileException = false;
		WifiManager.WifiLock wifiLock = Util.createWifiLockHigh(mContext,
		        "NicoRoVideoLoader");
		try {
		    if (wifiLock != null) {
		        wifiLock.setReferenceCounted(false);
		        wifiLock.acquire();
		    }
			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==========");
			}

			// ネットワーク接続成功したらカウントリセット
			mNetworkIORetryCount = NETWORK_IO_RETRY_COUNT;

			InfoDataPart part = getTempInfoDataPart();
			String lastCacheLength = part.lastCacheLength;
			String lastContentLength = part.lastContentLength;
			String ifModifiedSince = part.ifModifiedSince;
			String ifRange = part.ifRange;
			// lastCacheLengthおよびlastContentLengthの数値チェックは事前に済み
			boolean startDownload;
			AbstractMultiRandomAccessFile 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;
				try {
                    tempFile = createMultiRandomAccessFile(mTempFilePath, false,
                            mContentLength, seekOffsetWrite);
				} catch (IOException e) {
				    fileException = true;
				    throw e;
				}
				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;
				try {
                    tempFile = createMultiRandomAccessFile(mTempFilePath, true,
                            mContentLength, seekOffsetWrite);
                } catch (IOException e) {
                    fileException = true;
                    throw e;
				}
				lastModified = Util.getFirstHeaderValue(httpResponse, HEADER_LAST_MODIFIED);
				etag = Util.getFirstHeaderValue(httpResponse, HEADER_ETAG);
				// ETagは付いてないかもしれないので値チェック
				if (etag == null) {
				    etag = ifRange;
				}
				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());
				}
				try {
                    // 作成時に予めファイルサイズ変更
                    tempFile = createMultiRandomAccessFile(mTempFilePath, true,
                            mContentLength, 0L);
                } catch (IOException e) {
                    fileException = true;
                    throw e;
                }
				lastModified = Util.getFirstHeaderValue(httpResponse, HEADER_LAST_MODIFIED);
				etag = Util.getFirstHeaderValue(httpResponse, HEADER_ETAG);
				mLastModified = lastModified;
				mETag = etag;
				startDownload = true;
				if (httpRequest.getFirstHeader("If-Modified-Since") != null
				        || httpRequest.getFirstHeader("If-Range") != null) {
				    // キャッシュはあったが最初から取得
		            mHandler.obtainMessage(MSG_ID_SHOW_INFO_TOAST,
		                    mContext.getResources().getString(
		                            R.string.toast_old_cache_ignore, mVideoV));
				}
			} break;
			case HttpStatus.SC_FORBIDDEN: {
				// Cookie取り直してもう１回再取得を試みる
			    --mForbiddenRetryCount;
				if (mForbiddenRetryCount >= 0) {
					mHttpClient.getCookieStore().clear();
					mCookie = NicoroAPIManager.getCookieNicoHistory(
							mHttpClient, mVideoV, mCookieUserSession,
							mEcoType, mUserAgent);
//					mHttpRequest = null;
					return true;
				} else {
					// エラー
				    notifyHttpErrorToListener(statusLine);
					startDownload = false;
				}
			} break;
			case HttpStatus.SC_GATEWAY_TIMEOUT: {
			    // Gatewayのタイムアウト：サーバー不調時に発生
			    // とりあえずもう一回アクセスを試みる
			    --mGatewayTimeoutRetryCount;
                if (mGatewayTimeoutRetryCount >= 0) {
//                    mHttpRequest = null;
                    waitForNetworkError();
                    return true;
                } else {
                    // エラー
                    notifyHttpErrorToListener(statusLine);
                    startDownload = false;
                }
			} break;
			default: {
				// エラー
                notifyHttpErrorToListener(statusLine);
				startDownload = false;
			} break;
			}

			if (startDownload) {
			    // カウントリセット
			    mForbiddenRetryCount = FORBIDDEN_RETRY_COUNT;
			    mGatewayTimeoutRetryCount = GATEWAY_TIMEOUT_RETRY_COUNT;

			    if (mHasStarted) {
                    if (mEventListener != null) {
                        mEventListener.onRestarted(this);
                    }
			    } else {
    				if (mEventListener != null) {
    					mEventListener.onStarted(this);
    				}
    				mHasStarted = true;
			    }
				// 読み込み
				httpEntity = httpResponse.getEntity();
				inDownload = httpEntity.getContent();
				mInDownload.set(inDownload);
				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;
							}
							try {
							    writeFile(tempFile, buffer, offset);
			                } catch (IOException e) {
			                    fileException = true;
			                    throw e;
							}
							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;
					}
					try {
                        writeFile(tempFile, buffer, offset);
	                } catch (IOException e) {
	                    fileException = true;
	                    throw e;
	                }
					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();
            Log.d(LOG_TAG, errorMessage, e);
            --mNetworkIORetryCount;
            if (mNetworkIORetryCount >= 0) {
                // ネットワークエラーはひとまずリトライ
                waitForNetworkError();
                return true;
            } else {
                if (mEventListener != null) {
                    mEventListener.onOccurredError(this, errorMessage);
                }
            }
		} catch (IOException e) {
            String errorMessage = e.toString();
            Log.d(LOG_TAG, errorMessage, e);
		    if (fileException) {
		        // ファイル書き込みエラーは即エラーで終了
    			if (mEventListener != null) {
    				mEventListener.onOccurredError(this, errorMessage);
    			}
		    } else {
		        --mNetworkIORetryCount;
	            if (mNetworkIORetryCount >= 0) {
	                // ネットワークエラーはひとまずリトライ
                    waitForNetworkError();
	                return true;
	            } else {
	                if (mEventListener != null) {
	                    mEventListener.onOccurredError(this, errorMessage);
	                }
	            }
		    }
		} catch (IllegalStateException e) {
		    // HttpClientを強制シャットダウンしたときに飛んでくる可能性
            String errorMessage = e.toString();
            Log.d(LOG_TAG, errorMessage, e);
            --mNetworkIORetryCount;
            if (mNetworkIORetryCount >= 0) {
                // ネットワークエラーはひとまずリトライ
                waitForNetworkError();
                return true;
            } else {
                if (mEventListener != null) {
                    mEventListener.onOccurredError(this, errorMessage);
                }
            }
		} catch (NumberFormatException e) {
		    // キャッシュ情報かHTTPヘッダが壊れている可能性
            String errorMessage = e.toString();
            Log.d(LOG_TAG, errorMessage, e);
            --mNetworkIORetryCount;
            if (mNetworkIORetryCount >= 0) {
                // ネットワークエラーとしてひとまずリトライ
                waitForNetworkError();
                return true;
            } else {
                if (mEventListener != null) {
                    mEventListener.onOccurredError(this, errorMessage);
                }
            }
		} finally {
            // 情報保存
            if (lastModified != null /*&& etag != null*/ && !wasSaveInfo) {
                saveInfo(lastModified, etag);
            }

//            mHttpRequest = null;
            if (httpEntity != null) {
                try {
                    httpEntity.consumeContent();
                } catch (IOException e) {
                    Log.e(LOG_TAG, e.toString(), e);
                }
            }
            mHttpClient.getConnectionManager().shutdown();
            // 同時にcloseすると不具合出る可能性があるので一回のみに
			if (inDownload != null && mInDownload.getAndSet(null) != null) {
			    if (DEBUG_LOGD) {
			        Log.d(LOG_TAG, "VideoLoader close start at finally");
			    }
			    FileUtil.closeIgnoreException(inDownload);
                if (DEBUG_LOGD) {
                    Log.d(LOG_TAG, "VideoLoader close end at finally");
                }
			}
			if (wifiLock != null && wifiLock.isHeld()) {
			    wifiLock.release();
			}
		}
		return false;
	}

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

	private void readInfo(String path, InfoData infoData) {
        File file = new File(path);
		JSONObject jsonLoad = Util.createJSONFromFile(file);
        if (DEBUG_LOGD) {
            if (jsonLoad == null) {
                Log.d(LOG_TAG, Log.buf().append(path)
                        .append(" load failed, continue.").toString());
            } else {
                Log.d(LOG_TAG, Log.buf().append("Load ").append(path)
                        .append(": ").append(jsonLoad.toString()).toString());
            }
        }
		readInfo(jsonLoad, infoData);

		// 数値に異常がないかチェックして、異常があればキャッシュ情報破棄
		boolean invalid = false;
		if (!checkNumber(infoData.high)) {
		    infoData.high.reset();
		    invalid = true;
		}
		if (!checkNumber(infoData.mid)) {
            infoData.mid.reset();
            invalid = true;
		}
        if (!checkNumber(infoData.low)) {
            infoData.low.reset();
            invalid = true;
        }
		if (invalid) {
		    // キャッシュファイルいったん破棄
		    file.delete();
		}
	}

	private static boolean checkNumber(InfoDataPart part) {
        try {
            if (part.lastContentLength != null) {
                Long.parseLong(part.lastContentLength);
            }
            if (part.lastCacheLength != null) {
                Long.parseLong(part.lastCacheLength);
            }
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
	}

    void readInfo(JSONObject jsonLoad, InfoData infoData) {
		infoData.reset();
		if (jsonLoad == null) {
			// ない場合はそのまま処理継続
		} else {
			readInfo(infoData.high, jsonLoad, null);
            readInfo(infoData.low, jsonLoad, LOW_VIDEO_SUFFIX);
            readInfo(infoData.mid, jsonLoad, MID_VIDEO_SUFFIX);
		}
	}

	private void readInfo(InfoDataPart part, JSONObject jsonLoad,
	        String keySuffix) {
	    String headerLastModified = HEADER_LAST_MODIFIED;
	    String headerEtag = HEADER_ETAG;
	    String headerContentLength = HEADER_CONTENT_LENGTH;
	    String keyCachedLength = KEY_CACHED_LENGTH;
	    if (!TextUtils.isEmpty(keySuffix)) {
	        headerLastModified += keySuffix;
	        headerEtag += keySuffix;
	        headerContentLength += keySuffix;
	        keyCachedLength += keySuffix;
	    }
        part.ifModifiedSince = jsonLoad.optString(headerLastModified, null);
        part.ifRange = jsonLoad.optString(headerEtag, null);
        part.lastContentLength = jsonLoad.optString(headerContentLength, null);
        part.lastCacheLength = jsonLoad.optString(keyCachedLength, null);
	}

	// XXX 何故か分からないが思いのほか処理が重くなるときがある
	private void saveInfo(String lastModified, String etag) {
        if (mIsPriorityLowerThanWrite) {
            android.os.Process.setThreadPriority(THREAD_PRIORITY_WRITE);
        }
        long startTime = 0;
        if (DEBUG_LOGV) {
            startTime = SystemClock.elapsedRealtime();
        }
		assert lastModified != null;
//		assert etag != null;
		assert mContentLength > 0;
		AbstractMultiRandomAccessFile 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 = createJsonForSaveInfo(lastModified, etag,
		            seekOffsetWrite);
			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);
		}
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append(getClass().getSimpleName())
                    .append("#saveInfo() time=")
                    .append(SystemClock.elapsedRealtime() - startTime)
                    .append("ms")
                    .toString());
        }
        if (mIsPriorityLowerThanWrite) {
            android.os.Process.setThreadPriority(mThreadPriority);
        }
	}

	JSONObject createJsonForSaveInfo(String lastModified,
            String etag, long seekOffsetWrite) throws JSONException {
        JSONObject jsonSave = new JSONObject();
        if (mEcoType == ECO_TYPE_LOW) {
            saveInfoSame(mTempInfoData.high, jsonSave, null);

            saveInfo(mTempInfoData.low, jsonSave, LOW_VIDEO_SUFFIX,
                    mContentLength, lastModified,
                    etag, seekOffsetWrite);

            saveInfoSame(mTempInfoData.mid, jsonSave, MID_VIDEO_SUFFIX);
        } else if (mEcoType == ECO_TYPE_MID) {
            saveInfoSame(mTempInfoData.high, jsonSave, null);

            saveInfoSame(mTempInfoData.low, jsonSave, LOW_VIDEO_SUFFIX);

            saveInfo(mTempInfoData.mid, jsonSave, MID_VIDEO_SUFFIX,
                    mContentLength, lastModified,
                    etag, seekOffsetWrite);
        } else {
            saveInfo(mTempInfoData.high, jsonSave, null,
                    mContentLength, lastModified,
                    etag, seekOffsetWrite);

            saveInfoSame(mTempInfoData.low, jsonSave, LOW_VIDEO_SUFFIX);

            saveInfoSame(mTempInfoData.mid, jsonSave, MID_VIDEO_SUFFIX);
        }
        jsonSave.put(KEY_LAST_PLAYED, System.currentTimeMillis());
	    return jsonSave;
	}

    private static void saveInfo(InfoDataPart part, JSONObject jsonSave,
            String keySuffix, long contentLength, String lastModified,
            String etag, long cachedLength) throws JSONException {
        String headerLastModified = HEADER_LAST_MODIFIED;
        String headerEtag = HEADER_ETAG;
        String headerContentLength = HEADER_CONTENT_LENGTH;
        String keyCachedLength = KEY_CACHED_LENGTH;
        if (!TextUtils.isEmpty(keySuffix)) {
            headerLastModified += keySuffix;
            headerEtag += keySuffix;
            headerContentLength += keySuffix;
            keyCachedLength += keySuffix;
        }

        jsonSave
            .put(headerContentLength, contentLength)
            .put(headerLastModified, lastModified)
            .put(headerEtag, etag)
            .put(keyCachedLength, cachedLength);
    }

    private static void saveInfoSame(InfoDataPart part, JSONObject jsonSave,
            String keySuffix) throws JSONException {
        String headerLastModified = HEADER_LAST_MODIFIED;
        String headerEtag = HEADER_ETAG;
        String headerContentLength = HEADER_CONTENT_LENGTH;
        String keyCachedLength = KEY_CACHED_LENGTH;
        if (!TextUtils.isEmpty(keySuffix)) {
            headerLastModified += keySuffix;
            headerEtag += keySuffix;
            headerContentLength += keySuffix;
            keyCachedLength += keySuffix;
        }

        jsonSave
            .put(headerContentLength, part.lastContentLength)
            .put(headerLastModified, part.ifModifiedSince)
            .put(headerEtag, part.ifRange)
            .put(keyCachedLength, part.lastCacheLength);
    }

    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(EXT_JSON);
        assert end > 0;
        data.videoV = fileName.substring(0, end);
        return data;
    }

	@Override
	public int readFromNativeCallback(int bufSize, byte[] buffer) {
	    try {
	        long startTime = 0;
	        if (DEBUG_LOGD_READ) {
	            startTime = SystemClock.elapsedRealtime();
	        }
    		AbstractMultiRandomAccessFile 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.length)
    							.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;
    				    }
    				    try {
                            tempFile.readFully(buffer, 0, bufSize);
                            ret = bufSize;
    				    } catch (EOFException e) {
    				        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).append(" time=")
    					.append(SystemClock.elapsedRealtime() - startTime)
    					.append("ms")
    					.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);
		}

		InfoDataPart part = getTempInfoDataPart();
		String lastContentLength = part.lastContentLength;
		String lastCacheLength = part.lastCacheLength;
		String ifModifiedSince = part.ifModifiedSince;
		String ifRange = part.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(Context context, String videoV) {
        return isFinishedCacheCommon(context, videoV,
                HEADER_CONTENT_LENGTH,
                KEY_CACHED_LENGTH);
    }

    public static final boolean isFinishedCacheMid(Context context, String videoV) {
        return isFinishedCacheCommon(context, videoV,
                HEADER_CONTENT_LENGTH + MID_VIDEO_SUFFIX,
                KEY_CACHED_LENGTH + MID_VIDEO_SUFFIX);
    }

    public static final boolean isFinishedCacheLow(Context context, String videoV) {
        return isFinishedCacheCommon(context, videoV,
                HEADER_CONTENT_LENGTH + LOW_VIDEO_SUFFIX,
                KEY_CACHED_LENGTH + LOW_VIDEO_SUFFIX);
    }

    private static final boolean isFinishedCacheCommon(Context context,
            String videoV, String headerContentLength, String keyCachedLength) {
        String tempInfoPath = createCacheInfoPath(context, videoV);
        JSONObject jsonLoad = Util.createJSONFromFile(tempInfoPath);
        if (jsonLoad == null) {
            return false;
        }
        String lastContentLength = jsonLoad.optString(headerContentLength, null);
        String lastCacheLength = jsonLoad.optString(keyCachedLength, null);
        if (lastContentLength != null && lastContentLength.equals(lastCacheLength)) {
            // TODO とりあえず更新チェック省略
            return true;
        } else {
            return false;
        }
    }

	public static final boolean hasAnyCacheFile(Context context, String videoV) {
		File json = new File(createCacheInfoPath(context, videoV));
		if (json.exists()) {
			return true;
		}
		File dat = new File(createCacheFilePath(context, videoV));
		if (dat.exists()) {
			return true;
		}
		File lowDat = new File(createLowCacheFilePath(context, videoV));
		if (lowDat.exists()) {
			return true;
		}
        File midDat = new File(createMidCacheFilePath(context, videoV));
        if (midDat.exists()) {
            return true;
        }
		return false;
	}

    @Override
    public int getEcoType() {
        return mEcoType;
    }

    @Override
	public String getMovieType() {
		if (mMovieType == null) {
			AbstractMultiRandomAccessFile 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);
//					}
					read = tempFile.readTemporary(0, head);
					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;
	}

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

    @Override
	public boolean isCacheCompleted() {
		return mIsCacheCompleted;
	}

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

    @Override
	public String getUrl() {
	    return mUrl;
	}

	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 {
            long startTime = 0;
    		AbstractMultiRandomAccessFile tempFile = mTempFile;
    		if (DEBUG_LOGD_SEEK) {
                startTime = SystemClock.elapsedRealtime();
    			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();
//    					if (ret <= 0) {
//    					    ret = -1;
//    					}
//    				}
    				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).append(" time=")
                        .append(SystemClock.elapsedRealtime() - startTime)
                        .append("ms")
    			        .toString());
    		}
    		return ret;
        } catch (Throwable e) {
            Log.e(LOG_TAG, e.toString(), e);
            return -1;
        }
	}

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

    private AbstractMultiRandomAccessFile createMultiRandomAccessFile(String file,
            boolean write) throws FileNotFoundException {
        return new MultiRandomAccessFile(file, write);
//        return new MultiRandomAccessFileMmap(file, true);
    }

    private AbstractMultiRandomAccessFile createMultiRandomAccessFile(String file,
            boolean write, long length, long seekOffsetWrite) throws IOException {
        AbstractMultiRandomAccessFile tempFile = mTempFile;
        if (tempFile == null) {
            mTempFile = createMultiRandomAccessFile(file, write);
            tempFile = mTempFile;
            tempFile.setLength(length);
            tempFile.seekWrite(seekOffsetWrite);
        } else if (tempFile.equalFilePath(file)) {
            if (tempFile.length() != length) {
                tempFile.setLength(length);
            }
            if (tempFile.tellWrite() != seekOffsetWrite) {
                tempFile.seekWrite(seekOffsetWrite);
            }
        } else {
            // XXX 指定ファイルが異なるときは、ひとまず閉じて再生成
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "mTempFile is recreated by different file path");
            }
            tempFile.close();
            mTempFile = createMultiRandomAccessFile(file, write);
            tempFile = mTempFile;
            tempFile.setLength(length);
            tempFile.seekWrite(seekOffsetWrite);
        }
        return tempFile;
    }

    public static String getStreamTempDir(Context context) {
        SharedPreferences sharedPreference = Util.getDefaultSharedPreferencesMultiProcess(context);
        String ret = sharedPreference.getString(context.getString(
                R.string.pref_key_select_cache_dir), null);
        if (ret == null) {
            ret = getStreamTempDirDefault(context);
        }
        return ret;
    }

    public String getStreamTempDir() {
        return getStreamTempDir(mContext);
    }

    public static String getStreamTempDirDefault(Context context) {
        APILevelWrapper api = APILevelWrapper.createInstance();
        return new File(api.getExternalFilesDir(context, null),
                "vcache").getAbsolutePath();
    }

    private void writeFile(AbstractMultiRandomAccessFile tempFile,
            byte[] buffer, int offset) throws IOException {
        if (mIsPriorityLowerThanWrite) {
            android.os.Process.setThreadPriority(THREAD_PRIORITY_WRITE);
            tempFile.write(buffer, 0, offset);
            android.os.Process.setThreadPriority(mThreadPriority);
        } else {
            tempFile.write(buffer, 0, offset);
        }
    }

    private void setFinish() {
        synchronized (mSync) {
            mIsFinish = true;
            mSync.notifyAll();
        }
    }

    private void waitForNetworkError() {
        synchronized (mSync) {
            if (!mIsFinish) {
                try {
                    mSync.wait(WAIT_FOR_NETWORK_ERROR);
                } catch (InterruptedException e) {
                }
            }
        }
    }

    private InfoDataPart getTempInfoDataPart() {
        if (mEcoType == ECO_TYPE_LOW) {
            return mTempInfoData.low;
        } else if (mEcoType == ECO_TYPE_MID) {
            return mTempInfoData.mid;
        } else {
            return mTempInfoData.high;
        }
    }

    void setContentLengthForTest(long contentLength) {
        mContentLength = contentLength;
    }

    InfoData getTempInfoDataForTest() {
        return mTempInfoData;
    }

    public static long getCacheSizeSettings(Context context,
            SharedPreferences sharedPreference) {
        return 1024 * 1024 * Long.parseLong(sharedPreference.getString(
                context.getString(R.string.pref_key_strage_cache_size),
                "512"));
    }
}
