package dareka.processor.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dareka.NLConfig;
import dareka.common.FileUtil;
import dareka.common.Logger;
import dareka.common.TextUtil;
import dareka.extensions.SystemEventListener;
import dareka.processor.util.Flv2mp4;


// ロック用Staticオブジェクト
class LockObj {
    static Object FileMoveLock = new Object();
    static Object FileStoreLock = new Object();
}

/**
 *
 * Abstraction of cache handling.
 *
 */
// TODO Too fragile! Have to be managed the state... 
public class Cache extends CacheManager {
    public static final String FLV = ".flv";
    public static final String SWF = ".swf";
    public static final String MP4 = ".mp4";

    private VideoDescriptor video;
    private String desc;
    private File cacheFileNew; // [nl] 新規に作成する場合のキャッシュファイル
    private File tmpFile;

    public static void init() {
        CacheManager.init();
    }

	// [nl] フォルダ間移動
	public boolean moveTo(String dir) {
		if (dirList.contains(dir)) {
			File cacheFile = getCacheFile();
			File toFile = new File(dir2File.get(dir), cacheFile.getName());
			return moveCache(cacheFile, toFile);
		}
		return false;
	}
	
	// [nl] キャッシュファイルに移動
	private boolean moveCache(File src, File dst) {
		if (src != null && dst != null &&
				src.exists() && !dst.exists() && !src.equals(dst)) {
			// まずリネームしてみる
            video2FileLock.writeLock().lock();
			try {
				if (src.renameTo(dst)) {
                    video2File_put(video, dst);
					return true;
				}
			} finally {
				video2FileLock.writeLock().unlock();
			}
			// 駄目ならコピーしてみる。
			// 
			// 直接コピーするとinit()を呼び出すAPIが並行して発行された場合に
			// コピー途中のものを認識して不具合が起きる可能性があるので、
			// まず一時ファイルにコピーしてから改めてリネームする。
			// また、コピーの場合はI/Oを直列化させて競合状態を防ぐ。
			// 
			File tmp = new File(dst.getPath().concat(".tmp"));
			synchronized (LockObj.FileMoveLock) {
				if (tmp.exists() || FileUtil.copy(src, tmp) == -1)
					return false;
				// コピーしている間にsrcが変更される場合があるかも
				if (!src.exists()) {
					if ((src = video2File_get(video)) == null || !src.exists()) {
						tmp.delete();
						return false;
					}
					dst = new File(dst.getParentFile(), src.getName());
				}
			}
            video2FileLock.writeLock().lock();
			try {
				if (tmp.renameTo(dst)) {
					src.delete();
					video2File_put(video, dst);
					return true;
				} else {
					tmp.delete(); // 失敗(まず有り得ないと思うけど…)
				}
			} finally {
				video2FileLock.writeLock().unlock();
			}
		}
		return false; 
	}
	
	// [nl] キャッシュファイルを指定パスにコピー
	public boolean copyTo(String dstPath) {
		return copyTo(new File(dstPath));
	}
	public boolean copyTo(File dstFile) {
		File cacheFile = video2File_get(video);
		if (cacheFile != null && cacheFile.exists()) {
			return FileUtil.copy(cacheFile, dstFile) != -1;
		}
		return false;
	}
	
	// [nl] 指定パスからキャッシュファイルにコピー
	public boolean copyFrom(String srcPath) {
		return copyFrom(new File(srcPath));
	}
	public boolean copyFrom(File srcFile) {
		File cacheFile = getCacheFile();
		// 一旦テンポラリファイルにコピーして成功すればキャッシュに置き換える
		File tmp = new File(cacheFile.getPath().concat(".tmp"));
		if (!tmp.exists() && FileUtil.copy(srcFile, tmp) != -1) {
			video2FileLock.writeLock().lock();
			try {
				cacheFile.delete();
				if (tmp.renameTo(cacheFile)) {
					video2File_put(video, cacheFile);
					return true;
				}
			} finally {
				video2FileLock.writeLock().unlock();
			}
		}
		return false;
	}
	
	/**
	 *  [nl] キャッシュを取り除く。Extension が阻害することもある。
	 * @param id キャッシュ ID
	 * @return キャッシュを取り除いたら true
	 */
    @Deprecated
	public static boolean remove(String id) {
		return remove(altIdToVideoDescriptor(id));
	}
    
    /**
	 *  [nl] キャッシュを取り除く。Extension が阻害することもある。
	 * @param video キャッシュの VideoDescriptor
	 * @return キャッシュを取り除いたら true
	 */
	public static boolean remove(VideoDescriptor video) {
		File cacheFile = video2File_get(video);
		if (cacheFile == null || !cacheFile.exists()) {
			return false;
		}
		NLEventSource eventSource = null;
		if (NLShared.INSTANCE.countSystemEventListeners() > 0) {
			eventSource = new NLEventSource(null, null, new Cache(video));
			int result = NLShared.INSTANCE.notifySystemEvent(
					SystemEventListener.CACHE_REMOVING, eventSource, true);
			if (result != SystemEventListener.RESULT_OK) {
				return false;
			}
		}
		if (cacheFile.delete()) {
			video2File_put(video, null);
			unregisterVideoDescriptorIfUnused(video);
			if (eventSource != null) {
				NLShared.INSTANCE.notifySystemEvent(
						SystemEventListener.CACHE_REMOVED, eventSource, false);
			}
			return true;
		}
		return false;
	}
	
    // [nl] 一時ファイルを取り除く
    @Deprecated
    public static boolean removeTmp(String id) {
        return removeTmp(altIdToVideoDescriptor(id));
    }
    
	// [nl] 一時ファイルを取り除く
	public static boolean removeTmp(VideoDescriptor video) {
		File tmp = video2Tmp.get(video);
		if (tmp != null && getDLFlag(video) == false) {
			if (tmp.exists() && tmp.delete()) {
                video2Tmp.remove(video);
                unregisterVideoDescriptorIfUnused(video);
                File mapFile = getCacheTmpMappedRangesFile(tmp);
                if (mapFile != null && mapFile.exists()) {
                    mapFile.delete();
                    video2MappedRanges.remove(video);
                }
				//[xmlfix]
				removeXmlCache(TYPE_TEMPLIST, null);
				return true;
			}
		}
		return false;
	}
	
	// [nl] 指定IDに関連する以下のファイルを全て取り除く
	// 通常キャッシュ、エコノミーキャッシュ、それらの一時ファイル
	public static boolean removeAll(String id) {
		return removeAll(id, true);
	}
    public static boolean removeAll(String id, boolean rmDL) {
        boolean removed = false;
        id = id.replaceFirst("low", ""); // for compatibility
        Set<VideoDescriptor> videos = id2Videos.get(id);
        if (videos == null) {
            return false;
        }
        synchronized (videos) {
            for (VideoDescriptor video : videos) {
                if (rmDL) removed |= removeDL(video);
                removed |= remove(video) | removeTmp(video);
            }
        }
		return removed;
	}
	
	// [nl] ダウンロード中のキャッシュを削除予約する
	private static boolean removeDL(VideoDescriptor video) {
		if (Cache.getDLFlag(video)) {
			NLShared.INSTANCE.addDeleteSet(video.getId());
			return true;
		}
		return false;
	}
	
	// [nl] 動画URLの識別子から拡張子をセットする
	protected void setPostfix(String vType) {
        String postfix = null;
		switch (vType.charAt(0)) {
		case 'v':
			postfix = FLV; break;
		case 's':
			postfix = SWF; break;
		case 'm':
			postfix = MP4; break;
		case '.':
			postfix = vType; break;
		}
        if (postfix != null) {
            video = video.replacePostfix(postfix);
        }
	}
	
	public String getPostfix() {
		return video.getPostfix();
	}

    public static void cleanup() {
        // [nl] 一時ファイル削除無効(何もしない)
    }

    // [nl] コンストラクタ
    @Deprecated
    public Cache(String cacheId) {
        this(cacheId, FLV);
    }

    @Deprecated
    public Cache(String cacheId, String postfix) {
        this.video = altIdToVideoDescriptor(cacheId, postfix);
        setPostfix(postfix);
    }

    @Deprecated
    public Cache(String cacheId, String postfix, String desc) {
        this.video = altIdToVideoDescriptor(cacheId, postfix);
        this.desc = TextUtil.stripZeroWithChars(desc); // [nl]
        setPostfix(postfix);
    }

    public Cache(VideoDescriptor video) {
        this.video = video;
    }

    public Cache(VideoDescriptor video, String desc) {
        this.video = video;
        this.desc = TextUtil.stripZeroWithChars(desc);
    }

    // [nl] 新規キャッシュファイルのインスタンスを作成
    // コンストラクタで作成せずに実際に必要になる時まで遅延させる
    private void setCacheFileNew() {
    	if (desc != null) {
    		cacheFileNew = getDescribedCacheFile(desc, false);
    	} else {
            cacheFileNew = new File(cacheDir, videoDescriptorToPrefixString(video, false) + video.getPostfix());
    	}
    }
    
    private File getCacheTmpFileSub() {
    	if (desc != null) {
    		return getDescribedCacheFile(desc, true);
    	} else {
            return new File(cacheDir, NLTMP_ + videoDescriptorToPrefixString(video, true)
                    + video.getPostfix());
    	}
    }

	// [nl] タイトルを取得（無い場合はID）
	public String getTitle() {
		String filename = this.getCacheFileName();
		Matcher m = CACHE_FILE_PATTERN.matcher(filename);
		if (m.find()) {
			if (m.group(4) != null)
				return m.group(4);
			else
				return m.group(1);
		}
        m = DMC_CACHE_FILE_PATTERN.matcher(filename);
        if (m.find()) {
			if (m.group(8) != null)
				return m.group(8);
			else
				return m.group(1);
		}
        return "";
	}
	
	// [nl] ファイル名のタイトルを変更する
	public boolean setTitle(String newTitle) {
		desc = newTitle;
		setCacheFileNew();
		File cacheFile = video2File_get(video);
		File newFile = new File(cacheFile.getParentFile(), cacheFileNew.getName());
		boolean result = moveCache(cacheFile, newFile);
        if (result) {
            video2File_put(video, newFile);
        }
        return result;
	}
    
    public void unmarkLow() {
        if (!video.isLow()) {
            return;
        }
        video = video.replaceLow(false);
        // renameTo tmp
        File tmpFile = video2Tmp.get(video);
        if (tmpFile != null && tmpFile.exists()) {
            try {
                fixTmpFilename();
            } catch (IOException e) {
                Logger.info("failed to unmark low tmp file");
            }
        }
        // renameTo cache
        File cacheFile = video2File_get(video);
        if (cacheFile != null) {
            setCacheFileNew();
            moveCache(cacheFile, cacheFileNew);
        }
        registerVideoDescriptor(video);
    }

    private class DirInfo implements Comparable<DirInfo> {
    	public String path;
    	public String name;
    	public int depth;
    	public DirInfo(String path, String name, int depth) {
    		this.path = path;
    		this.name = name;
    		this.depth = depth;
    	}
    	
    	// 名前部分が長い・深い順
    	public int compareTo(DirInfo e) {
    		if (this.name.length() == e.name.length())
    			return this.depth == e.depth ? 0 : (this.depth < e.depth ? 1 : -1);
    		return this.name.length() < e.name.length() ? 1 : -1;
    	}
    }

	// [nl]フォルダ名をフィルタとして、ストア先を探す
	protected String suggestStoreFolder() {
		ArrayList<DirInfo> dirs = new ArrayList<DirInfo>();
		for (String dir : dirList) {
			if (dir.length() == 0 || NLConfig.find("storeFilterIgnore", dir))
				continue;
			String[] parts = dir.split("/");
			int n = parts.length;
			dirs.add(new DirInfo(dir, tidyTitle(parts[n-1].toLowerCase()), n));
		}
		// 検索順にソートして、 "/" を除いておく
		Collections.sort(dirs);
		
		String test = getCacheFileName().toLowerCase();
		for (DirInfo entry : dirs) {
			String[] patterns = entry.name.split(" +");
			boolean match = true;
			for (String p : patterns) {
				if (p.charAt(0) == '-') {
					// NOT
					if (test.contains(p.substring(1)))
						match = false;
				} else {
					// AND
					if (!test.contains(p))
						match = false;
				}
			}
			if (match)
				return entry.path;
		}
		
		return "";
	}
	
	// [nl] タイトル文字列を有る程度整形する
	// 		全角英数字→半角英数字
	// 		複数の半角全角スペースを1つの半角スペースに
	private static final Pattern spacePattern = Pattern.compile("[ 　]+");
	public static String tidyTitle(String oldTitle) {
		char title[] = spacePattern.matcher(oldTitle).replaceAll(" ").trim().toCharArray();
		
		for (int i = 0; i < title.length; i++) {
			char c = title[i];
			if ('０' <= c && c <= '９')
				c = (char)(c - '０' + '0');
			else if ('Ａ' <= c && c <= 'Ｚ')
				c = (char)(c - 'Ａ' + 'A');
			else if ('ａ' <= c && c <= 'ｚ')
				c = (char)(c - 'ａ' + 'a');
			title[i] = c;
		}
		return new String(title);
	}
	
	// [nl] 実体参照(Entity Reference)を置換する
	private static String replaceER(String src) {
		return src.replaceAll("&amp;" , "&")
				  .replaceAll("&lt;"  , "<")
				  .replaceAll("&gt;"  , ">")
				  .replaceAll("&quot;", "\"")
				  .replaceAll("&apos;", "'");
	}

    protected File getDescribedCacheFile(String desc, boolean tmpfile) {
        StringBuffer decodedDesc = new StringBuffer(desc.length());

        Matcher m = NUMBER_CHARACTER_REFERENCE_PATTERN.matcher(desc);

        while (m.find()) {
            m.appendReplacement(decodedDesc,
                    Character.toString((char) Integer.parseInt(m.group(1))));
        }
        m.appendTail(decodedDesc);

        // [nl] タイトル整形
        if (Boolean.getBoolean("tidyTitle")) {
        	desc = tidyTitle(desc);
        }

        if (tmpfile) {
            return new File(cacheDir, NLTMP_ + Cache.getSanitizedDescription(
                    videoDescriptorToPrefixString(video, true) + "_" + desc)
                    + video.getPostfix());
        } else {
            return new File(cacheDir, Cache.getSanitizedDescription(
                    videoDescriptorToPrefixString(video, false) + "_" + desc)
                    + video.getPostfix());
        }
    }

    public static String getSanitizedDescription(String decodedDesc) {
        String fileNameCharset = System.getProperty("fileNameCharset");

        String narrowedDesc = narrowCharset(decodedDesc, fileNameCharset);

        // 実体参照を更に実体参照するケースがあるので二回置換する
        narrowedDesc = replaceER(replaceER(narrowedDesc));

        char descArray[] = narrowedDesc.toCharArray();

        for (int i = 0; i < descArray.length; i++) {
        	char c = descArray[i];
            switch (c) {
            case '\\': case '\u00a5': c = '￥'; break;
            case '/': c = '／';	break;
            case ':':	c = '：';		break;
            case '*':	c = '＊';	break;
            case '?':	c = '？';	break;
            case '"':	c = '”';	break;
            case '<':	c = '＜';	break;
            case '>':	c = '＞';	break;
            case '|':	c = '｜';	break;
            case '\u203e':// c = '~'; break;
            case '\u301c':// c = '〜'; break;
            case '\u2016': case '\u2212': case '\u00a2':
            case '\u00a3': case '\u00ac':
            	c = '-'; break;
            }

            descArray[i] = c;
        }
        return new String(descArray);
    }

    // 一旦目的の文字コードにしてから戻して、対応してない文字を '?' にする
    static String narrowCharset(String str, String charsetName) {
        String narrowedDesc = str;
        if (!charsetName.equals("")) {
            try {
                narrowedDesc =
                        new String(str.getBytes(charsetName), charsetName);
                // characters which are not supported by the charset
                // become '?'
            } catch (UnsupportedEncodingException e) {
                Logger.warning(e.toString());
            }

        }
        return narrowedDesc;
    }

    /**
     * VideoDescriptorを返す
     */
    public VideoDescriptor getVideoDescriptor() {
        return video;
    }

    /**
     * AltIdを返す
     */
    public String getId() {
        return videoDescriptorToAltId(video);
    }

    /**
     * [nl] 動画ID(末尾に"low"を含まない)を返す
     * @return 動画ID
     * @since NicoCache_nl+111111mod
     */
    public String getVideoId() {
        return video.getId();
    }

    /**
     * [nl] エコノミーキャッシュか？
     * @return エコノミーキャッシュなら true
     * @since NicoCache_nl+111111mod
     */
    public boolean isLow() {
    	return video.isLow();
    }

    public File getCacheFile() {
    	// [nl] 排他制御が必要なので常にid2Fileから取得する
    	File cacheFile = video2File_get(video);
    	if (cacheFile != null) {
    		setDescribe(cacheFile, true);
    		return cacheFile;
    	} else if (cacheFileNew == null) {
    		setCacheFileNew();
    	}
    	return cacheFileNew;
    }

    public String getCacheFileName() {
        return getCacheFile().getName();
    }

    public String getURLString() {
        return getCacheFile().toURI().toString();
    }

    public void setDescribe(String desc) {
//    	cacheFile = getDescribedCacheFile(desc);
    	this.desc = desc;
    	cacheFileNew = null;
    }

    // [nl] キャッシュファイルから設定
    private void setDescribe(File cacheFile, boolean setAux) {
        if (setDescribeCore(cacheFile.getName(), setAux)) {
            cacheFileNew = null;
        }
    }
    
    private void setDescribeFromTmp(File tmpCacheFile, boolean setAux) {
        String filename = tmpCacheFile.getName();
        filename = filename.substring(6);
        setDescribeCore(filename, setAux);
    }
    
    private boolean setDescribeCore(String filename, boolean setAux) {
    	Matcher m = CACHE_FILE_PATTERN.matcher(filename);
    	if (m.matches()) {
    		if(m.group(4) != null) desc = m.group(4);
    		if(setAux) video = video.replacePostfix(m.group(5));
            return true;
    	}
        m = DMC_CACHE_FILE_PATTERN.matcher(filename);
    	if (m.matches()) {
    		if(m.group(8) != null) desc = m.group(8);
    		if(setAux) {
                boolean low = m.group(3).equals("low");
                if (low != video.isLow()) {
                    video = video.replaceLow(low);
                }
            }
            return true;
    	}
        return false;
    }
    
    @Deprecated
    public void setTmpDescribe(String title) throws IOException {
        String filename = NLTMP_ + Cache.getSanitizedDescription(
                videoDescriptorToPrefixString(video, true)+ "_" + title) + video.getPostfix();
        File newName = new File(cacheDir, filename);
        MappedRanges mr = getCacheTmpMappedRanges();
        File file = getCacheTmpFile();
        if (file != null && mr != null && file.renameTo(newName)) {
            video2Tmp.put(video, newName);
            tmpFile = newName;
            mr.renameTo(getCacheTmpMappedRangesFile());
        }
    }
    
    protected void fixTmpFilename() throws IOException {
        String filename = NLTMP_ + Cache.getSanitizedDescription(
                videoDescriptorToPrefixString(video, true)+ "_" + desc) + video.getPostfix();
        File newName = new File(cacheDir, filename);
        MappedRanges mr = getCacheTmpMappedRanges();
        File file = getCacheTmpFile();
        if (file != null && mr != null && file.renameTo(newName)) {
            video2Tmp.put(video, newName);
            tmpFile = newName;
            mr.renameTo(getCacheTmpMappedRangesFile());
        }
    }

    public long length() {
        return getCacheFile().length();
    }

    public long tmpLength() throws IOException {
        File file = getCacheTmpFile();
        if (file != null) {
            return file.length();
        } else {
            return 0;
        }
    }
    
    /**
     * 一時ファイルにキャッシュ済みのサイズを返す．
     * @return キャッシュ済みのサイズ．一時ファイルがない場合などエラー時は-1．
     */
    public long tmpCachedSize() {
        if (!video2Tmp.containsKey(video)) {
            return -1;
        }
        MappedRanges mr = getCacheTmpMappedRanges();
        if (mr == null) {
            return -1;
        }
        return mr.getMappedSize();
    }
    
    /**
     * 一時ファイルのmapファイルから最終的なサイズを返す．
     * @return 最終的なサイズ．一時ファイルがない場合などエラー時は-1．
     */
    public long tmpFinalSize() {
        if (!video2Tmp.containsKey(video)) {
            return -1;
        }
        MappedRanges mr = getCacheTmpMappedRanges();
        if (mr == null) {
            return -1;
        }
        return mr.getSize();
    }

    public boolean exists() {
        return getCacheFile().exists();
    }

    public void touch() {
    	File cacheFile = getCacheFile();
    	if (cacheFile.exists()) {
    		cacheFile.setLastModified(System.currentTimeMillis());
    		statCache.remove(video);
    	}
    }

    public InputStream getInputStream() throws IOException {
        //cacheFile.setLastModified(System.currentTimeMillis());
        if (Boolean.getBoolean("touchCache")) touch();
        return new FileInputStream(getCacheFile());
    }

    /**
     * 一時ファイルの入力ストリームを得る
     * @return 一時ファイルの入力ストリーム
     * @throws IOException
     */
    public InputStream getTmpInputStream() throws IOException {
        File cacheTmpFile = getCacheTmpFile();
        if (cacheTmpFile == null) {
            throw new FileNotFoundException();
        }
        return new FileInputStream(cacheTmpFile);
    }

    /**
     * 一時ファイルの出力ストリームを得る
     * [nl] ファイル名が長過ぎる場合は1文字ずつ切り詰めてリトライする
     * @param append 追記する場合true
     * @return 一時ファイルの出力ストリーム
     * @throws IOException
     */
    public RandomAccessFile getTmpOutputStream() throws IOException {
    	String truncated = null;
    	do {
    		try {
    			RandomAccessFile out = createTmpOutputStream();
    			if (truncated != null) {
    				Logger.warning("title truncated: " + truncated);
    			}
    			return out;
    		} catch (FileNotFoundException e) {
    			if (desc == null && desc.length() < 60 || !cacheDir.exists()) {
    				throw e;
    			}
    			video2Tmp.remove(video);
    			tmpFile = null;
    			
    			// 多分ファイル名が長いので1文字ずつ切り詰めてみる
    			truncated = desc.substring(0, desc.length() - 1);
    			setDescribe(truncated);
    			continue;
    		}
    	} while (desc != null && desc.length() > 0);
    	
    	// ここまで来たならタイトル無しで返す
    	setDescribe(null);
    	return createTmpOutputStream();
    }

    // [nl]
    private RandomAccessFile createTmpOutputStream() throws IOException {
        File cacheTmpFile = getCacheTmpFileWithSet();
        return new RandomAccessFile(cacheTmpFile, "rw");
    }

	// キャッシュを適切な位置・名前で格納する。
	// [nl] エコノミー版が有れば消すように。振り分け関係を追加
	//		エコノミー版があればその場所に保存するように (#027, #028)
    public void store() throws IOException {
// eco=1を実装したので、同一IDの通常･エコノミーキャッシュが
// 同時に作成されないよう排他制御
		synchronized (LockObj.FileStoreLock) {

			//[xmlfix] 一時ファイル一覧のXMLキャッシュを削除
			removeXmlCache(TYPE_TEMPLIST, null);

			File cacheTmpFile = getCacheTmpFile();
            File cacheMapFile = getCacheTmpMappedRangesFile();

			File parentDir = cacheDir;
			String storeFolder = "";
            if (hasSuperiorCache()) {
// キャッシュ完了時、より良いキャッシュがあったら保存しない
                cacheTmpFile.delete();
                if (cacheMapFile != null) {
                    cacheMapFile.delete();
                }
                video2Tmp.remove(video);
                video2MappedRanges.remove(video);
                Logger.info("nomal cache already exists, removed: " + video);
                return;
            }
            
// すでにあるlow等のキャッシュファイルからタイトルと保存先を取得
// 最初に見つけたものを使う
            Set<VideoDescriptor> oldVideos = id2Videos.get(video.getId());
            if (oldVideos != null) {
                for (VideoDescriptor v : oldVideos) {
                    File f = video2File_get(v);
                    if (f != null && f.exists()) {
                        setDescribe(f, false);
                        parentDir = f.getParentFile();
                        storeFolder = video2dir.get(f);
                        break;
                    }
                }
            }
// low等のキャッシュファイルと一時ファイルを削除
            for (VideoDescriptor lesserVideo : getLesserCachedVideos()) {
                Cache.remove(lesserVideo);
                Cache.removeTmp(lesserVideo);
            }

			if (parentDir == cacheDir && Boolean.getBoolean("storeFilter")) {
				storeFolder = suggestStoreFolder();
				if (!storeFolder.equals("")) {
					Logger.info("storing folder: " + storeFolder);
					parentDir = dir2File.get(storeFolder);
				}
			}
			
			VideoDescriptor oldvideo = video;
			video = video.stripSrcId();
			registerVideoDescriptor(video);
            
			setCacheFileNew();
			File cacheFile = new File(parentDir, cacheFileNew.getName());
			
			if (!moveCache(cacheTmpFile, cacheFile)) {
				cacheTmpFile.delete();
			}
            if (cacheMapFile != null) {
                cacheMapFile.delete();
            }
			
			video2Tmp.remove(oldvideo);
			video2MappedRanges.remove(oldvideo);
			unregisterVideoDescriptorIfUnused(oldvideo);
		}
	}

    public boolean deleteTmp() throws IOException {
    	File cacheTmpFile = video2Tmp.remove(video);
        File cacheMapFile = getCacheTmpMappedRangesFile();
    	tmpFile = null;
        if (cacheTmpFile == null) {
            return false;
        }
        unregisterVideoDescriptorIfUnused(video);
        cacheMapFile.delete();
        video2MappedRanges.remove(video);
        return cacheTmpFile.delete();
    }

    boolean existsTmp() { // [nl]
    	return tmpFile != null && tmpFile.exists();
    }

    protected synchronized File getCacheTmpFile() {
        if (tmpFile == null) {
            File knownTmpFile = video2Tmp.get(video);
            if (knownTmpFile != null) {
                setDescribeFromTmp(knownTmpFile, false);
                tmpFile = knownTmpFile;
            }
        }
        return tmpFile;
    }
    
    // tmpFileのtest and setをatomicにするためにsynchronizedが必要。
    protected synchronized File getCacheTmpFileWithSet() {
        if (tmpFile == null) {
            File knownTmpFile = video2Tmp.get(video);
            if (knownTmpFile == null) {
                tmpFile = getCacheTmpFileSub();
            } else {
                setDescribeFromTmp(knownTmpFile, false);
                tmpFile = knownTmpFile;
            }
        }
        video2Tmp.put(video, tmpFile);
        registerVideoDescriptor(video);
        return tmpFile;
    }

    protected static ConcurrentHashMap<VideoDescriptor, MappedRanges> video2MappedRanges =
            new ConcurrentHashMap<VideoDescriptor, MappedRanges>();
    
    public MappedRanges getCacheTmpMappedRanges() {
        MappedRanges mr = video2MappedRanges.get(video);
        if (mr != null) {
            return mr;
        }
        File mapFile = getCacheTmpMappedRangesFile();
        if (mapFile == null) {
            return null;
        }
        mr = new MappedRanges(mapFile);
        MappedRanges old = video2MappedRanges.putIfAbsent(video, mr);
        if (old != null) {
            mr = old;
        }
        try {
            mr.load();
        } catch (IOException e) {
            Logger.error(e);
        }
        return mr;
    }
        
    protected File getCacheTmpMappedRangesFile() {
        return getCacheTmpMappedRangesFile(getCacheTmpFile());
    }
    
    protected static File getCacheTmpMappedRangesFile(File cacheTmpFile) {
        if (cacheTmpFile == null) {
            return null;
        }
        File dir = cacheTmpFile.getParentFile();
        if (!cacheTmpFile.getName().startsWith(NLTMP_)) {
            return null;
        }
        String cacheTmpFilename = cacheTmpFile.getName();
        String mapFilename = cacheTmpFilename.substring(0, cacheTmpFilename.length() - 1) + "_";
        File mapFile = new File(dir, mapFilename);
        // mapファイルだけが存在している場合は削除
        if (mapFile.exists() && !cacheTmpFile.exists()) {
            mapFile.delete();
        }
        return mapFile;
    }
    
    // Flv2Mp4で変換済みのファイル一覧
    private final static ConcurrentHashMap<VideoDescriptor, File> video2ConvertedMp4 =
            new ConcurrentHashMap<VideoDescriptor, File>();
    
    /**
     * FLVからMPEG4に変換したファイルを返す．
     * まだ変換されていないキャッシュの処理には時間がかかる．
     */
    protected File getConvertedMp4FromFlv() {
        File convertedFile;
        // 変換済みのデータがあるか
        convertedFile = video2ConvertedMp4.get(video);
        if (convertedFile != null) {
            // 変換が終わるのを待って返す
            synchronized (convertedFile) {
                // 変換に失敗した場合はnullに変わる
                convertedFile = video2ConvertedMp4.get(video);
                return convertedFile;
            }
        }
        File tmpfile;
        try {
            if (System.getProperty("flv2mp4TmpDir") == null || System.getProperty("flv2mp4TmpDir").isEmpty()) {
                tmpfile = File.createTempFile("nl_" + getId() + "_", Cache.MP4);
            } else {
                tmpfile = File.createTempFile("nl_" + getId() + "_", Cache.MP4,
                        new File(System.getProperty("flv2mp4TmpDir")));
            }
        } catch (IOException e) {
            return null;
        }
        convertedFile = video2ConvertedMp4.putIfAbsent(video, tmpfile);
        if (convertedFile != null) { // 割りこまれた
            tmpfile.delete();
            // 変換が終わるのを待って返す
            synchronized (convertedFile) {
                // 変換に失敗した場合はnullに変わる
                convertedFile = video2ConvertedMp4.get(video);
                return convertedFile;
            }
        }
        tmpfile.deleteOnExit();
        synchronized (tmpfile) {
            boolean success = Flv2mp4.convert(getCacheFile(), tmpfile);
            if (!success) {
                tmpfile.delete();
                video2ConvertedMp4.remove(video);
                return null;
            }
        }
        return tmpfile;
    }
    
    /**
     * 再エンコードされた動画かを判定する．
     * 不明なときはfalseを返す．
     */
    public boolean isReEncoded() {
        Boolean result = isReEncodedStrictly();
        if (result == null) {
            return false;
        } else {
            return result;
        }
    }
    
    /**
     * 再エンコードされた動画かを判定する．
     * 不明なときはnullを返す．
     */
    public Boolean isReEncodedStrictly() {
        String id = video.getId();
        VideoDescriptor v = video2Video.get(video);
        if (v == null) {
            return null;
        }
        if (v.isDmc() || v.isLow()) {
            return true;
        }
        if (!Cache.MP4.equals(v.getPostfix())) {
            // mp4以外はdmcが無いはずなのでfalseを返しても良いけど
            return null;
        }
        
        File file = video2File_get(video);
        if (file == null || !file.exists()) {
            file = video2Tmp.get(video);
            if (file == null || !file.exists()) {
                return null;
            }
            MappedRanges mr = getCacheTmpMappedRanges();
            if (!mr.isMapped(0, ReEncodingInfo.REQUIRED_HEADER_LENGTH)) {
                return null;
            }
        }
        return ReEncodingInfo.check(id, file);
    }
    
    /**
     * 削除できるキャッシュの一覧を返す．
     */
    protected VideoDescriptor[] getLesserCachedVideos() {
        VideoDescriptor criterion = this.video;
        ArrayList<VideoDescriptor> result = new ArrayList<VideoDescriptor>();
        Set<VideoDescriptor> videos = id2Videos.get(criterion.getId());
        if (videos == null) {
            return new VideoDescriptor[0];
        }
        for (VideoDescriptor video : videos) {
            if (criterion.isSuperiorThan(video)) {
                result.add(video);
            }
        }
        if (Boolean.getBoolean("removeReEncodedCache")) {
            if (criterion.isDmc() && !criterion.isLow()
                    && criterion.getPostfix().equals(Cache.MP4)) {
                // 高画質なdmc動画より再エンコードされた非dmc動画は悪い
                for (VideoDescriptor video : videos) {
                    if (!video.isDmc()) {
                        Cache cache = new Cache(video);
                        if (video.isLow() ||
                                video.getPostfix().equals(Cache.MP4)
                                && cache.exists() && cache.isReEncoded()) {
                            result.add(video);
                        }
                    }
                }
            } else if (!criterion.isDmc() && !criterion.isLow()
                    && criterion.getPostfix().equals(Cache.MP4)
                    && !isReEncoded()) {
                // 再エンコードされていない非dmc動画よりdmc動画は悪い
                for (VideoDescriptor video : videos) {
                    if (video.isDmc()) {
                        result.add(video);
                    }
                }
            }
        }
        return result.toArray(new VideoDescriptor[0]);
    }
    
    protected boolean hasSuperiorCache() {
        VideoDescriptor criterion = this.video;
        Set<VideoDescriptor> videos = id2Videos.get(criterion.getId());
        if (videos == null) {
            return false;
        }
        for (VideoDescriptor video : videos) {
            if (video.isSuperiorThan(criterion)) {
                File f = video2File_get(video);
                if (f != null && f.exists()) {
                    return true;
                }
            }
        }
        if (Boolean.getBoolean("removeReEncodedCache")) {
            if (!criterion.isDmc() && (criterion.isLow() ||
                    criterion.getPostfix().equals(Cache.MP4)
                    && isReEncoded())) {
                // 再エンコードされた非dmc動画より高画質なdmc動画が良い
                for (VideoDescriptor video : videos) {
                    if (video.isDmc() && !video.isLow()) {
                        Cache cache = new Cache(video);
                        if (cache.exists()) {
                            return true;
                        }
                    }
                }
            } else if (criterion.isDmc() && criterion.getPostfix().equals(Cache.MP4)) {
                // dmc動画より再エンコードされていない非dmc動画が良い
                for (VideoDescriptor video : videos) {
                    if (!video.isDmc() && !video.isLow()
                            && video.getPostfix().equals(Cache.MP4)) {
                        Cache cache = new Cache(video);
                        if (cache.exists() && !cache.isReEncoded()) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }
}
