package dareka.processor.impl;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.SortedSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dareka.Main;
import dareka.common.Logger;
import dareka.processor.HttpHeader;
import dareka.processor.HttpRequestHeader;
import dareka.processor.LocalFileResource;
import dareka.processor.Processor;
import dareka.processor.Resource;
import dareka.processor.StringResource;
import dareka.processor.util.AudioExtractor;
import dareka.processor.util.CommentDownloader;

// [nl] cacheフォルダ関係の処理
public class CacheDirProcessor implements Processor {
//    private static final String[] SUPPORTED_METHODS = new String[] { "GET" };
    private static final String[] SUPPORTED_METHODS = new String[] {
        HttpHeader.GET, HttpHeader.POST };
    private static final Pattern CACHE_DIR_PATTERN = Pattern
        .compile("^http://[^/]+\\.nicovideo\\.jp/cache/?(|.+)$");
    
    public String[] getSupportedMethods() {
        return SUPPORTED_METHODS;
    }

    public Pattern getSupportedURLAsPattern() {
        return CACHE_DIR_PATTERN;
    }

    public String getSupportedURLAsString() {
        return null;
    }

    private static final Pattern ALT_ID_PATTERN = Pattern.compile(
            "(([a-z]{2}[0-9]+)(?:low)?(?:\\[\\w+,\\d+,\\d+\\]\\w*\\.(?:flv|mp4))?)");
    private static final Pattern LOCAL_FLV_PATTERN = Pattern.compile(
            "^[a-z]{2}[0-9]+(?:low)?(?:\\[\\w+,\\d+,\\d+\\]\\w*)?(?:\\.(?:flv|swf|mp4)){1,2}$"); // double suffix for cachemanager workaround
    private static final Pattern AUDIO_EXTRACT_PATTERN = Pattern.compile(
            "^[a-z]{2}[0-9]+(?:low)?(?:\\[\\w+,\\d+,\\d+\\]\\w*\\.(?:flv|mp4))?\\.(?:mp3|m4a)$");
    private static final Pattern COMMENT_API_PATTERN = Pattern.compile(
            "^_?(\\w{2}\\d+)\\.xml(?:|\\?(.*))$");
    private static final Pattern LST_API_PATTERN = Pattern.compile(
            "^(add|trim)(r)?(list/[^\\?]+)(?:|\\?(.+))$");
    private static final Pattern SEARCH_API_PATTERN = Pattern.compile(
            "^r?search/([^\\?]+)(\\?.+)?$");
    private static final Pattern INFO_API_PATTERN = Pattern.compile(
            "^info(/v2)?\\?(.+)$");
    private static final Pattern MOVID_AUDIO_REDIRECTER_PATTERN = Pattern.compile(
            "^[a-z]{2}[0-9]+(?:low)?(?:\\[\\w+,\\d+,\\d+\\]\\w*\\.(?:flv|mp4))?(/auto)?/(movie|audio)$");
    
    public Resource onRequest(HttpRequestHeader requestHeader, Socket browser)
            throws IOException {
        String path = requestHeader.getPath();
        
        if (path.length() < 7)
            return null;
        path = path.substring(7);
        
        if (HttpHeader.POST.equals(requestHeader.getMethod())) {
            String postString = Main.getRewriterProcessor().getPostString(
                    requestHeader, browser, null);
            if (postString.length() > 0) {
                if (path.equals("info")) {
                    return processCacheInfoAPI(postString, 1);
                } else if (path.equals("info/v2")) {
                    return processCacheInfoAPI(postString, 2);
                }
            }
            return null;
        }
        
        // 単一キャッシュを対象としないAPIを先に処理する
        Matcher m;
        if ((m = SEARCH_API_PATTERN.matcher(path)).matches()) {
            String query = URLDecoder.decode(m.group(1), "UTF-8");
            boolean desc = m.group(2) != null && m.group(2).contains("order=d");
            boolean regex = path.startsWith("r");
            StringBuilder sb = createJsonStringBuilder();
            for (VideoDescriptor v : SearchRewriter.searchVideo(query, regex, desc).keySet()) {
                if (Cache.writeJSON(v, sb)) sb.append(",\n");
            }
            return createJsonStringResource(sb);
        }
        if ((m = INFO_API_PATTERN.matcher(path)).matches()) {
            return processCacheInfoAPI(m.group(2), "/v2".equals(m.group(1)) ? 2 : 1);
        }
        
        // 以降は単一キャッシュを対象とするこれまでの処理
        String altid = "";
        String smid = "";
        VideoDescriptor video = null;
        Cache cache = null;
        Matcher mTypeId = ALT_ID_PATTERN.matcher(path);
        if(mTypeId.find()) {
            altid = mTypeId.group(1);
            smid = mTypeId.group(2);
            video = Cache.altIdToVideoDescriptor(altid);
            cache = new Cache(video);
        }
        
        // [nl] ローカルFLV（キャッシュを送る）
        if (Boolean.getBoolean("localFlv") &&
                (m = LOCAL_FLV_PATTERN.matcher(path)).matches()) {
            if (!cache.exists()) {
                return StringResource.getNotFound();
            }
            Logger.info("Local Flv: " + cache.getId() + " " + cache.getTitle());
            if (Boolean.getBoolean("touchCache")) {
                cache.touch();
            }
            Resource r = new LocalFileResource(cache.getCacheFile());
            LimitFlvSpeedListener.addTo(r);
            r.addCacheControlResponseHeaders(12960000);
            return r;
        }
        
        // 音声ファイル抽出
        if ((m = AUDIO_EXTRACT_PATTERN.matcher(path)).matches()) {
            AudioExtractor ae = new AudioExtractor(
                    cache, path, requestHeader.getMessageHeader("User-Agent"));
            return ae.extract();
        }
        
        // コメント取得
        if ((m = COMMENT_API_PATTERN.matcher(path)).matches()) {
            String params = m.group(2);
            if (path.startsWith("_")) {  // 投稿者コメント(旧形式)
                return CommentDownloader.getResource(smid, "1", requestHeader, browser);
            } else if (params == null) { // パラメータ無し(旧形式)
                return CommentDownloader.getResource(smid, "0", requestHeader, browser);
            } else { // パラメータ有りなら新形式
                return CommentDownloader.getURLResource(m.group(1),
                        params.contains("has_owner_thread=1"), requestHeader);
            }
        }
        
        // 適切なダウンロードパスへリダイレクトさせる(夏.01)
        Matcher mMovieAudio = MOVID_AUDIO_REDIRECTER_PATTERN.matcher(path);
        if (mMovieAudio.matches()) {
            if (mMovieAudio.group(1) != null) {
                // auto select
                video = Cache.getPreferredCachedVideo(smid);
                if (video != null) {
                    cache = new Cache(video);
                }
            }
            if (cache == null || !cache.exists()) {
                return StringResource.getNotFound();
            }
            String ext = cache.getPostfix();
            String base = "http://www.nicovideo.jp/cache/" +
                    Cache.videoDescriptorToAltId(video);
            if (mMovieAudio.group(2).equals("movie")) {
                if (video.isDmc()) {
                    // dmcのaltidには拡張子が入っている
                    return StringResource.getRedirect(base);
                } else {
                    return StringResource.getRedirect(base + ext);
                }
            } else {
                if (ext.equals(Cache.FLV) || ext.equals(Cache.SWF)) {
                    return StringResource.getRedirect(base + ".mp3");
                } else if (ext.equals(Cache.MP4)) {
                    return StringResource.getRedirect(base + ".m4a");
                }
            }
            return StringResource.getNotFound();
        }
        // [nl] crossdomain.xmlにNotFound（同一ドメインなので本来必要ない）
        else if (path.equals("crossdomain.xml")) {
            return StringResource.getNotFound();
        }
        // [nl] キャッシュ管理ページ
        else if (path.equals("") || path.endsWith(".html")) {
            // [nl] swfからのリクエストなら簡易版にする
            if (requestHeader.getMessageHeader("x-flash-version") != null) {
                String flvList = "# my cache\r\n" + Cache.getFlvListForLocalFlv() + "\n";
                return createStringResource(flvList);
            }
            long start = System.currentTimeMillis();
            String str;
            if (path.equals("")) {
                str = Cache.getFlvList();
            } else {
                str = Cache.getFlvList("/local/" + path);
            }
            long elapsed = System.currentTimeMillis() - start;
            Logger.info("Local Flv List (" + elapsed + "msec)");
            return getStringResource(str, "text/html");
        }
        // [nl] リロードなしのflvのid一覧 (FxでのLocal Flv用)
        else if (path.equals("flvlist") || path.equals("flvlist/")) {
            String flvList = "# my cache\r\n" + Cache.getFlvListForLocalFlv() + "\n";
            return createStringResource(flvList);
        }
        else if (path.equals("flvlist/crossdomain.xml")) {
            return StringResource.getNotFound();    // ↑で"flvlist/"を追加したので
        }
        // [nl] キャッシュ管理ページ(ajax版)
        else if (path.equals("ajax")) {
            long start = System.currentTimeMillis();
            String str = Cache.getFlvListAjax();
            long elapsed = System.currentTimeMillis() - start;
            Logger.info("Local Flv List ajax (" + elapsed + "msec)");
            return getStringResource(str, "application/x-javascript");
        }
        //[nl] + JSON fix
        else if (path.endsWith(".json")) {
            String name = null;
            String str = null;
            long start = System.currentTimeMillis();
            if (path.equals("cachelist.json")) {
                name = "Local Cache List as JSON";
                str = Cache.getCacheListAsJson();
            } else if (path.equals("templist.json")) {
                name = "Local Temp Cache List as JSON";
                str = Cache.getTempListAsJson();
            } else if (path.equals("dirlist.json")) {
                name = "Local Cache Dir List as JSON";
                str = Cache.getDirListAsJson();
            } else if (path.equals("flvlist.json")) {
                name = "Local Flv List as JSON";
                str = Cache.getFlvListAsJson();
            }
            long elapsed = System.currentTimeMillis() - start;

            if (str != null) {
                Logger.info("%s (%d msec)", name, elapsed);
                return getStringResource(str, "application/json");
            }
            return StringResource.getNotFound();
        }
        //[xmlfix] XML取得
        else if (path.startsWith("getxml")) {
            if (path.length() < 7) {
                return StringResource.getBadRequest();
            }
            HashMap<String, String> query = new HashMap<String, String>();
            
            String key, value;
            for (String params : path.substring(7).split("&")) {
                String[] param = params.split("=",2);
                if (params.length() > 1) {
                    key = param[0]; value = param[1];
                    value = decodeURLForUTF8(value);
                    query.put(key, value);
                }
            }
            
            String name = "";
            String xml = null;
            String type = query.get("type");
            
            long start = System.currentTimeMillis();
            // ディレクトリ一覧
            if (Cache.TYPE_DIRLIST.equals(type)) {
                name = "Local Cache Dir List as XML";
                xml = Cache.getDirListAsXml(query);
            }
            // 一時ファイル一覧
            else if (Cache.TYPE_TEMPLIST.equals(type)) {
                name = "Local Temp Cache List as XML";
                xml = Cache.getTempListAsXml(query);
            }
            // キャッシュ一覧
            else if (Cache.TYPE_CACHELIST.equals(type)) {
                name = "Local Cache List as XML [" + query.get("path") + "]";
                xml = Cache.getCacheListAsXml(query);
            }
            // キャッシュ一覧（すべて）
            else if (Cache.TYPE_CACHELIST_ALL.equals(type)) {
                name = "Local Cache List as XML";
                xml = Cache.getCacheListAsXml(query);
            }
            long elapsed = System.currentTimeMillis() - start;
            
            if (xml != null) {
                //Logger.info("%s (%d msec)", name, elapsed);
                Logger.debug(name + " (" +  elapsed + " msec)");
                StringResource r = new StringResource(xml);
                //r.addNoCacheResponseHeaders();
                r.setResponseHeader("Expires", "Thu, 01 Dec 1994 16:00:00 GMT");
                r.setResponseHeader("Cache-Control", "no-store");
                r.setResponseHeader("Pragma", "no-cache");
                r.setResponseHeader("Content-Type", "application/xml; charset=UTF-8");
                return r;
            }
            return StringResource.getBadRequest();
        }
        // ログ(夏.??)
        else if (path.equals("log")) {
            if (ViewableLoggerHandler.isLogging()) {
                return new StringResource(ViewableLoggerHandler.getLogWithTableTag());
            } else {
                return new StringResource("NicoCache_nlのログをページから見るには、<br>enableLogHandler=true<br>としてください。");
            }
        }
        
        // デコード後、ajaxかどうかの判定
        path = URLDecoder.decode(path, "UTF-8");
        boolean ajax = false;
        if (path.startsWith("ajax_")) {
            ajax = true;
            path = path.substring(5);
        }

        String msg = "NG";
        // [nl] リストページ
        // [nl] ローカルFLVリスト→削除
        if (path.matches("rm(?:tmp)?\\?[a-z]{2}[0-9]+(?:low)?(?:\\[\\w+,\\d+,\\d+\\]\\w*\\.(?:flv|mp4))?(?:low)?")) { // tail low for cachemanager workaround
            if (path.startsWith("rmtmp")) {
                Logger.info("Remove Temporary: " + altid);
                if (Cache.removeTmp(video));
                    msg = "OK";
            } else {
                Logger.info("Remove Cache: " + smid);
                if (Cache.remove(video)) {
                    msg = "OK";
                }
            }
            if (msg.equals("NG")) {
                Logger.info("...failed.");
            }
        }
        else if (path.matches("rmall\\?[a-z]{2}[0-9]+")) {
            Logger.info("Remove All Cache: " + smid);
            if (Cache.removeAll(smid)) {
                msg = "OK";
            }
            if (msg.equals("NG")) {
                Logger.info("...failed.");
            }
        }
        // [nl] ローカルFLVリスト→タイトルを設定
        else if (path.matches("title\\?[a-z]{2}[0-9]+(low)?(?:\\[\\w+,\\d+,\\d+\\]\\w*\\.(?:flv|mp4))?(-.*)?")) {
            if (cache.exists()) {
                String title = null;
                String[] t = path.split("-", 2);
                if (t.length == 1) {
                    try {   // ニコニコから取得
                        title = new NicoCachingTitleRetriever(smid, requestHeader).call();
                    } catch (Exception e) {}
                } else {    // 引数で設定
                    title = t[1];
                }
                if (title != null) {
                    title = Cache.getSanitizedDescription(title);
                    if (cache.setTitle(title)) {
                        msg = "OK " + title;
                    }
                }
            }
        }
        // [nl] ローカルFLVリスト→フォルダ移動
        else if (path.matches("move\\?([a-z]{2}[0-9]+(low)?(?:\\[\\w+,\\d+,\\d+\\]\\w*\\.(?:flv|mp4))?)-.*")) {
            if (cache.exists()) {
                String[] t = path.split("-", 2);
                if (cache.moveTo(t[1]))
                    msg = "OK";
            }
        }
        // [nl] ローカルFLVリスト→topの時のみフォルダ移動
        else if (path.matches("topmove\\?([a-z]{2}[0-9]+(low)?(?:\\[\\w+,\\d+,\\d+\\]\\w*\\.(?:flv|mp4))?)-.*")) {
            if (cache.exists()) {
                String[] t = path.split("-", 2);
                if ("".equals(Cache.getPathFromVideoDescriptor(video)) && cache.moveTo(t[1]))
                    msg = "OK";
            }
        }
        // idからキャッシュ情報を取得(拡張性が足りなくてdmc対応できないので非推奨)
        else if (path.matches("info\\?[a-z]{2}[0-9]+(low)?(?:\\[\\w+,\\d+,\\d+\\]\\w*\\.(?:flv|mp4))?")) {
            VideoDescriptor preferredSmileVideo = Cache.getPreferredCachedVideo(smid, false, null);
            if (preferredSmileVideo != null) {
                Cache preferredSmileCache = new Cache(preferredSmileVideo);
                msg = "OK,"+(preferredSmileVideo.isLow()?"low":"")+","
                        +preferredSmileCache.getPostfix()+","
                        +preferredSmileCache.length()+",0,"
                        +preferredSmileCache.getTitle();
            } else {
                if (Cache.getDLFinalSize(video) != 0) {
                    msg = "OK,,,"+Cache.getDLSize(video)+","+Cache.getDLFinalSize(video)+",";
                } else if (Cache.getDLFinalSize(video.replaceLow(true)) != 0) {
                    msg = "OK,low,,"+Cache.getDLSize(video.replaceLow(true))+","+Cache.getDLFinalSize(video.replaceLow(true))+",";
                }
            }
        }
        // LST処理API
        else if ((m = LST_API_PATTERN.matcher(path)).matches()) {
            ajax = true;
            String cmd = m.group(1), file = m.group(3), value = m.group(4);
            boolean regex = m.group(2) != null;
            if (value == null) value = "";
            if ("add".equals(cmd) && value.length() > 0) {
                if (EasyRewriter.LST.append(file, value, regex)) {
                    msg = "OK";
                }
            } else if ("trim".equals(cmd)) {
                if (EasyRewriter.LST.trim(file, value.contains("type=smid"))) {
                    msg = "OK";
                }
            }
        }
        else {
            Logger.info("no method:" + path);
            return StringResource.getNotFound();
        }
        
        // 何かしらハンドラが有った場合は最後に応答を返す
        if (ajax) {
            return createStringResource(msg);
        } else {
            String redir = null;
            String nlFrom = requestHeader.getParameter("nl-region");
            if (nlFrom != null
                    && (nlFrom.equals("cache") || nlFrom.equals("local")))
                redir = requestHeader.getParameter("nl-from");
            else
                redir = "http://www.nicovideo.jp/";
            StringResource r = StringResource.getRedirect(redir);
            r.addNoCacheResponseHeaders();
            return r;
        }
    }
    
    // キャッシュ情報一括取得API
    private Resource processCacheInfoAPI(String query, int version) {
        StringBuilder sb = createJsonStringBuilder();
        for (String id : query.split("[,\\s]+")) {
            String smid = id;
            if (id.matches("\\d+")) {
                if (id.length() >= 10)
                    smid = NLShared.INSTANCE.thread2smid(id);
                else
                    smid = Cache.id2Smid(id);
            }
            sb.append('"').append(id).append('"').append(':');
            if (smid == null ||
                    (version == 1 && !setCacheInfo(sb, smid)) ||
                    (version == 2 && !setCacheInfo2(sb, smid))) {
                sb.append("null");
            }
            sb.append(",\n");
        }
        return createJsonStringResource(sb);
    }
    
    private boolean setCacheInfo(StringBuilder sb, String videoId) {
        VideoDescriptor video = Cache.getPreferredCachedVideo(videoId);
        VideoDescriptor cachingSmile = null;
        VideoDescriptor cachingDmc = null;
        VideoDescriptor cachingSuperior = null;
        SortedSet<VideoDescriptor> videos = CacheManager.id2Videos.get(videoId);
        if (videos == null) {
            return false;
        }
        // キャッシュ進行中のVideoDescriptorを探す
        for (VideoDescriptor v : videos) {
            if (CacheManager.getDLFlag(v)) {
                if (v.isDmc()) {
                    if (cachingDmc == null) {
                        cachingDmc = v;
                    }
                } else {
                    if (cachingSmile == null) {
                        cachingSmile = v;
                    }
                }
                if (cachingSuperior == null) {
                    cachingSuperior = v;
                }
            }
        }
        if (video == null) { // キャッシュ無し
            if (cachingSuperior != null) {
                video = cachingSuperior;
            } else {
                return false;
            }
        }
        Cache cache = new Cache(video);
        
        sb.append('{');
        putKeyValueStingily(sb, "videoId", videoId);
        putKeyValueStingily(sb, "cacheId", Cache.videoDescriptorToAltId(video));
        putKeyValueStingily(sb, "economy", video.isLow());
        putKeyValueStingily(sb, "dmc", video.isDmc());
        if (video.isDmc()) {
            putVideoMode(sb, "dmcMovieType", video);
        }
        if (Cache.getDLFlag(video)) {
            putKeyValueStingily(sb, "size", Cache.getDLFinalSize(video));
            putKeyValueStingily(sb, "caching", true);
            putKeyValueStingily(sb, "cachingSize", Cache.getDLSize(video));
        } else {
            putKeyValueStingily(sb, "size", cache.length());
        }
        if (cache.exists()) {
            if (cachingSmile != null && !cachingSmile.isLow()) {
                putKeyValueStingily(sb, "normalCacheId", Cache.videoDescriptorToAltId(cachingSmile));
                putKeyValueStingily(sb, "normalSize", Cache.getDLFinalSize(cachingSmile));
                putKeyValueStingily(sb, "normalCachingSize", Cache.getDLSize(cachingSmile));
            }
            if (cachingDmc != null) {
                if (!cachingDmc.isLow()) {
                    putKeyValueStingily(sb, "dmcNormalCacheId", Cache.videoDescriptorToAltId(cachingDmc));
                    putKeyValueStingily(sb, "dmcNormalSize", Cache.getDLFinalSize(cachingDmc));
                    putKeyValueStingily(sb, "dmcNormalCachingSize", Cache.getDLSize(cachingDmc));
                    putVideoMode(sb, "dmcNormalMovieType", cachingDmc);
                } else {
                    putKeyValueStingily(sb, "dmcLowCacheId", Cache.videoDescriptorToAltId(cachingDmc));
                    putKeyValueStingily(sb, "dmcLowSize", Cache.getDLFinalSize(cachingDmc));
                    putKeyValueStingily(sb, "dmcLowCachingSize", Cache.getDLSize(cachingDmc));
                    putVideoMode(sb, "dmcLowMovieType", cachingDmc);
                }
            }
            if (cachingSuperior != null) {
                putKeyValueStingily(sb, "superiorCacheId", Cache.videoDescriptorToAltId(cachingSuperior));
                putKeyValueStingily(sb, "superiorSize", Cache.getDLFinalSize(cachingSuperior));
                putKeyValueStingily(sb, "superiorCachingSize", Cache.getDLSize(cachingSuperior));
                putKeyValueStingily(sb, "superiorIsDmc", cachingSuperior.isDmc());
                putKeyValueStingily(sb, "superiorIsEconomy", cachingSuperior.isLow());
                if (cachingSuperior.isDmc()) {
                    putVideoMode(sb, "superiorDmcMovieType", cachingSuperior);
                }
            }
            putKeyValueStingily(sb, "movieType", cache.getPostfix().substring(1));
            putKeyValueStingily(sb, "title", cache.getTitle());
            putKeyValueStingily(sb, "subFolder", Cache.getPathFromVideoDescriptor(video));
            putKeyValueStingily(sb, "ts", cache.getCacheFile().lastModified() / 1000L);
        }
        sb.replace(sb.length() - 1, sb.length(), "}");
        
        return true;
    }
    
    private boolean setCacheInfo2(StringBuilder sb, String videoId) {
        boolean filled;
        SortedSet<VideoDescriptor> videos = CacheManager.id2Videos.get(videoId);
        if (videos == null || videos.size() == 0) {
            return false;
        }
        
        // キャッシュァイルが削除されていた場合にcacheIdsに含めないため
        for (VideoDescriptor video : videos) {
            Cache.video2File_get(video);
        }
        
        sb.append('{');
        VideoDescriptor preferredSmile =
                Cache.getPreferredCachedVideo(videoId, false, null);
        VideoDescriptor preferredDmc =
                Cache.getPreferredCachedVideo(videoId, true, Cache.MP4);
        VideoDescriptor preferredDmcFlv =
                Cache.getPreferredCachedVideo(videoId, true, Cache.FLV);
        putKeyValue(sb, "preferred", videoDescriptorToAltId(
                Cache.getPreferredCachedVideo(videoId)));
        if (Boolean.getBoolean("useNotReEncodedCache") &&
                preferredSmile != null && !preferredSmile.isLow() &&
                Cache.MP4.equals(preferredSmile.getPostfix()) &&
                !(new Cache(preferredSmile)).isReEncoded()) {
            putKeyValue(sb, "preferredHTML5", videoDescriptorToAltId(preferredSmile));
            putKeyValue(sb, "preferredFlash", videoDescriptorToAltId(preferredSmile));
        } else {
            putKeyValue(sb, "preferredHTML5", videoDescriptorToAltId(
                    preferredDmc != null ? preferredDmc : preferredSmile));
            putKeyValue(sb, "preferredFlash", videoDescriptorToAltId(
                    preferredDmcFlv != null ? preferredDmcFlv : preferredSmile));
        }
        putKeyValue(sb, "preferredSmile", videoDescriptorToAltId(preferredSmile));
        putKeyValue(sb, "preferredDmc", videoDescriptorToAltId(preferredDmc));
        putKeyValue(sb, "preferredDmcFlv", videoDescriptorToAltId(preferredDmcFlv));
        
        // cacheIds
        putKey(sb, "cacheIds").append('[');
        for (VideoDescriptor video : videos) {
            putValue(sb, videoDescriptorToAltId(video)).append(',');
        }
        sb.replace(sb.length() - 1, sb.length(), "],");
        
        // cachings
        putKey(sb, "cachings").append('[');
        filled = false;
        for (VideoDescriptor video : videos) {
            if (Cache.getDLFlag(video)) {
                putValue(sb, videoDescriptorToAltId(video)).append(',');
                filled = true;
            }
        }
        if (filled) {
            sb.setLength(sb.length() - 1);
        }
        sb.append("],");
        
        // completes
        putKey(sb, "completes").append('[');
        filled = false;
        for (VideoDescriptor video : videos) {
            if (new Cache(video).exists()) {
                putValue(sb, videoDescriptorToAltId(video)).append(',');
                filled = true;
            }
        }
        if (filled) {
            sb.setLength(sb.length() - 1);
        }
        sb.append("],");
        
        // caches
        putKey(sb, "caches").append('{');
        for (VideoDescriptor video : videos) {
            Cache cache = new Cache(video);
            String altid = cache.getId();
            boolean complete = cache.exists();
            boolean caching = Cache.getDLFlag(video);
            
            putKey(sb, altid).append('{');
            putKeyValue(sb, "videoId", videoId);
            putKeyValue(sb, "cacheId", altid);
            putKeyValue(sb, "complete", complete);
            putKeyValue(sb, "economy", video.isLow());
            putKeyValue(sb, "dmc", video.isDmc());
            if (video.isDmc()) {
                putVideoMode(sb, "dmcMovieType", video);
            }
            putKeyValue(sb, "caching", caching);
            putKeyValue(sb, "movieType", cache.getPostfix().substring(1));
            if (complete) {
                putKeyValue(sb, "size", cache.length());
                putKeyValue(sb, "title", cache.getTitle());
                putKeyValue(sb, "subFolder", Cache.getPathFromVideoDescriptor(video));
                putKeyValue(sb, "filename", cache.getCacheFileName());
                putKeyValue(sb, "ts", cache.getCacheFile().lastModified() / 1000L);
            } else {
                File tmpFile = Cache.video2Tmp.get(video);
                putKeyValue(sb, "size", cache.tmpFinalSize());
                putKeyValue(sb, "cachingSize", cache.tmpCachedSize());
                if (tmpFile != null) {
                    putKeyValue(sb, "title", Cache.getTitleFromFilename(tmpFile.getName().substring(6)));
                    putKeyValue(sb, "subFolder", "");
                    putKeyValue(sb, "filename", tmpFile.getName());
                    putKeyValue(sb, "ts", tmpFile.lastModified() / 1000L);
                } else {
                    putKeyValue(sb, "title", null);
                    putKeyValue(sb, "subFolder", null);
                    putKeyValue(sb, "filename", null);
                    putKeyValue(sb, "ts", null);
                }
            }
            if (!video.isDmc() && !video.isLow()) {
                Boolean reencoded;
                if (Cache.MP4.equals(video.getPostfix()) &&
                        (reencoded = cache.isReEncodedStrictly()) != null) {
                    putKeyValue(sb, "reEncoded", reencoded);
                } else {
                    putKeyValue(sb, "reEncoded", null);
                }
            }
            sb.replace(sb.length() - 1, sb.length(), "},");
        }
        sb.replace(sb.length() - 1, sb.length(), "}");
        sb.append('}');
        return true;
    }
    
    private String videoDescriptorToAltId(VideoDescriptor video) {
        if (video == null) {
            return null;
        } else {
            return Cache.videoDescriptorToAltId(video);
        }
    }
    
    private StringBuilder putKey(StringBuilder sb, String key) {
        return sb.append('"').append(key).append('"').append(':');
    }
    
    private StringBuilder putValue(StringBuilder sb, boolean value) {
        return sb.append(Boolean.toString(value));
    }
    
    private StringBuilder putValue(StringBuilder sb, long value) {
        return sb.append(Long.toString(value));
    }
    
    private StringBuilder putValue(StringBuilder sb, String value) {
        if (value == null) {
            return sb.append("null");
        } else {
            return sb.append('"')
                    .append(value.replace("\\", "\\\\").replace("\"", "\\\""))
                    .append('"');
        }
    }

    private void putKeyValueStingily(StringBuilder sb, String key, boolean value) {
        if (value) {
            putValue(putKey(sb, key), value).append(',');
        }
    }
    
    private void putKeyValueStingily(StringBuilder sb, String key, long value) {
        if (value > 0) {
            putValue(putKey(sb, key), value).append(',');
        }
    }
    
    private void putKeyValueStingily(StringBuilder sb, String key, String value) {
        if (value != null && value.length() > 0) {
            putValue(putKey(sb, key), value).append(',');
        }
    }
    
    private void putKeyValue(StringBuilder sb, String key, boolean value) {
        putValue(putKey(sb, key), value).append(',');
    }
    
    private void putKeyValue(StringBuilder sb, String key, long value) {
        putValue(putKey(sb, key), value).append(',');
    }
    
    private void putKeyValue(StringBuilder sb, String key, String value) {
        putValue(putKey(sb, key), value).append(',');
    }

    private void putVideoMode(StringBuilder sb, String key, VideoDescriptor video){
        sb.append('"').append(key).append('"').append(":{");
        putKeyValue(sb, "videoMode", video.getVideoMode());
        putKeyValue(sb, "videoBitrate", video.getVideoBitrate());
        putKeyValue(sb, "audioBitrate", video.getAudioBitrate());
        sb.replace(sb.length() - 1, sb.length(), "},");
    }

    //[xmlfix]
    private String decodeURLForUTF8(String encodeStr) {
        String decodeStr = (encodeStr == null) ? "" : encodeStr;
        
        try {
            decodeStr = URLDecoder.decode(decodeStr, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            Logger.warning(e.toString());
        }
        
        return decodeStr;
    }
    
    private StringResource createStringResource(String str) {
        return getStringResource(str, null);
    }
    
    private StringResource getStringResource(String str, String contentType) {
        if (contentType == null) {
            contentType = "text/plain; charset=UTF-8";
        } else if (!contentType.contains("charset=")) {
            contentType += "; charset=UTF-8";
        }
        StringResource r = new StringResource(str);
        r.addCacheControlResponseHeaders(0);
        r.addResponseHeader(HttpHeader.CONTENT_TYPE, contentType);
        return r;
    }
    
    private StringBuilder createJsonStringBuilder() {
        StringBuilder sb = new StringBuilder(4096);
        return sb.append("{\n");
    }
    
    private StringResource createJsonStringResource(StringBuilder sb) {
        if (sb.length() > 2) {
            sb.replace(sb.length() - 2, sb.length(), "\n}");
        } else {
            sb.replace(0, sb.length(), "{}");
        }
        return getStringResource(sb.toString(), "application/json");
    }
}