package dareka.processor.impl;

import java.io.FileReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dareka.Main;
import dareka.NLConfig;
import dareka.common.LRUMap;
import dareka.common.Logger;
import dareka.common.TextUtil;
import dareka.common.json.*;
import dareka.processor.HttpHeader;
import dareka.processor.HttpRequestHeader;
import dareka.processor.HttpResponseHeader;

/**
 * [nl] watch ページをパースしてプレイヤーに設定する変数や動画情報の値を取得するクラス
 * <ul>
 * <li>原宿の場合は so.addVariable/var Video から取得
 * <li>GINZAの場合は watchAPIDataContainer から取得
 * <li>HTML5の場合は data-api-data から取得
 * <li>「将来的にニコ動が HTML5 ベースになる場合もこちらで吸収」
 *     →できた．なるほど神じゃねーの
 * </ul>
 * @since NicoCache_nl+120609mod
 */
public class WatchVars {
    // 原宿
    private static final Pattern SO_ADDVARIABLE_PATTERN = Pattern.compile(
            "so\\.addVariable\\(\\s*(\".+?\")\\s*,\\s*\"(.*?)\"\\);");
    private static final Pattern VAR_VIDEO_PATTERN = Pattern.compile(
            "<!--\\s+var Video = (\\{[\\s\\S]+?\\});\\s+(?:--></script>|var)");
    
    // Zero-GINZA
    private static final Pattern WATCH_API_DATA_CONTAINER_PATTERN = Pattern.compile(
            "<div id=\"watchAPIDataContainer\"[^>]*>([\\s\\S]+?)</div>");
    private static final Pattern PLAYLIST_DATA_CONTAINER_PATTERN = Pattern.compile(
            "<div id=\"playlistDataContainer\"[^>]*>([\\s\\S]+?)</div>");
    
    // HTML5
    private static final Pattern DATA_API_DATA_CONTAINER_PATTERN = Pattern.compile(
            "data-api-data=\"([^\"]+)\"");
    
    
    private static final Pattern WATCH_ID_PATTERN = Pattern.compile(
            "([a-z]{2})?(\\d+)");
    private static final Pattern WATCH_ECONOMY_MODE_PATTERN = Pattern.compile(
            "\\?(eco|lo)=1");
    
    private static final Map<String, WatchVars> cache =
            Collections.synchronizedMap(new LRUMap<String, WatchVars>());
    
    /**
     * WatchVars オブジェクトを取得する
     * <p>
     * 与えられた watch ページの内容から WatchVars オブジェクトを生成して返す。
     * 動画IDとタイトルが取得できた場合は {@linkplain NicoIdInfoCache} に記録する。
     * 更に生成したオブジェクトをキャッシュする。
     * <p>
     * 引数に http:// から始まる文字列、もしくは12文字以下の文字列を与えた場合、
     * 与えられた文字列を元に動画IDを取得し、その動画IDに対応する WatchVars
     * オブジェクトがキャッシュに残っているならそちらを返す。
     * <p>
     * 常に最新の情報を取得したい場合は、watch ページの内容を渡して WatchVars
     * オブジェクトを生成する必要が有る。
     * なお、オブジェクトの生成にはある程度の時間が必要なので、Rewriter
     * 内部からの呼び出し等で事前に watch ページを通っている事が自明である場合、
     * キャッシュを利用した方がパフォーマンス的に有利である。
     * 
     * @param content watch ページの内容、もしくは URL or 動画ID or スレッドID
     * @return WatchVars オブジェクト、キャッシュに残っていない場合のみ null
     */
    public static WatchVars get(String content) {
        if (content == null) {
            return null;
        }
        if (content.length() <= 12 || content.startsWith("http://")) {
            int pos = content.lastIndexOf('/');
            if (pos > 0) {
                content = content.substring(pos + 1);
            }
            Matcher m = WATCH_ID_PATTERN.matcher(content);
            if (!m.find()) {
                return null;
            }
            String videoId = m.group();
            if (TextUtil.isThreadId(videoId)) {
                videoId = NLShared.INSTANCE.thread2smid(videoId);
                if (videoId == null) {
                    return null;
                }
            }
            return cache.get(videoId);
        }
        return new WatchVars(content);
    }
    
    private String content, data;
    private Matcher dataMatcher;
    private boolean harajuku = false;
    private boolean ginza = false;
    private boolean rawJson = false;
    private JsonObject json;
    private JsonObject flashvars;
    private JsonObject dmcInfo;
    private boolean isDmc;
    private String videoId, videoTitle;
    private boolean isPeakTime, isPremium;
    private long duration;
    private List<String> videos = null;
    private List<String> audios = null;
    private Map<String, Boolean> qualityVideos = null;
    private Map<String, Boolean> qualityAudios = null;
    private HttpRequestHeader requestHeader;
    
    private WatchVars(String content) {
        this.content = content;
        dataMatcher = DATA_API_DATA_CONTAINER_PATTERN.matcher(content);
        if (!dataMatcher.find()) {
            dataMatcher = WATCH_API_DATA_CONTAINER_PATTERN.matcher(content);
            if (dataMatcher.find()) {
                ginza = true;
            } else {
                dataMatcher = null;
            }
        }
        if (dataMatcher != null) {
            data = TextUtil.unescapeHTML(dataMatcher.group(1));
            json = Json.parseObject(data);
        } else if (content.startsWith("{")) {
            data = content;
            json = Json.parseObject(data);
            rawJson = true;
        } else {
            data = content;
            json = harajuku2json(data);
            if (json != null) {
                harajuku = true;
            }
        }
        if (json != null) {
            if (dataMatcher == null && !json.get("flashvars").isNull()) {
                ginza = true;
            }
            if (!ginza && !harajuku) {
                dmcInfo = json.getObject("video", "dmcInfo");
                isDmc = dmcInfo != null;
                videoId = json.getString("video", "id");
                if (videoId != null) {
                    // titleとoriginalTitleの違いは何だろう
                    // originalDescriptionにはリンク処理後のHTML入っていたので
                    // titleも同様だとすればoriginalTitleを使うべき
                    videoTitle = json.getString("video", "originalTitle");
                    NicoCachingTitleRetriever.putTitleCache(videoId, videoTitle);
                    
                    isPeakTime = json.getBoolean("context", "isPeakTime");
                    isPremium = json.getBoolean("viewer", "isPremium");
                    
                    if (isDmc) {
                        videos = getArrayOfString(dmcInfo.getArray("session_api", "videos"));
                        audios = getArrayOfString(dmcInfo.getArray("session_api", "audios"));
                        
                        qualityVideos = getQualityAvailability(dmcInfo.getArray("quality", "videos"));
                        qualityAudios = getQualityAvailability(dmcInfo.getArray("quality", "audios"));
                    }
                    
                    JsonValue durationValue = json.get("video", "duration");
                    if (durationValue instanceof JsonNumber) {
                        duration = ((JsonNumber) durationValue).getLong();
                    }
                }
            } else if (ginza) {
                JsonValue v = json.get("flashvars");
                if (v instanceof JsonObject) {
                    flashvars = (JsonObject) v;
                }
                isDmc = json.getBoolean("flashvars", "isDmc");
                
                videoId = json.getString("flashvars", "videoId");
                if (videoId != null) {
                    videoTitle = json.getString("flashvars", "videoTitle");
                    NicoCachingTitleRetriever.putTitleCache(videoId, videoTitle);

                    isPeakTime = json.getBoolean("flashvars", "isPeakTime");
                    isPremium = json.getBoolean("viewerInfo", "isPremium");
                    
                    if (isDmc) {
                        String dmcInfoString = json.getString("flashvars", "dmcInfo");
                        if (dmcInfoString != null) {
                            dmcInfo = Json.parseObject(decodeURL(dmcInfoString));
                            videos = getArrayOfString(dmcInfo.getArray("session_api", "videos"));
                            audios = getArrayOfString(dmcInfo.getArray("session_api", "audios"));
                        } else {
                            isDmc = false;
                        }
                    }
                    
                    JsonValue durationValue = json.get("flashvars", "length");
                    if (durationValue instanceof JsonNumber) {
                        duration = ((JsonNumber) durationValue).getLong();
                    }
                }
            } else if (harajuku) {
                videoId = json.getString("videoId");
                if (videoId != null) {
                    videoTitle = json.getString("videoTitle");
                    NicoCachingTitleRetriever.putTitleCache(videoId, videoTitle);
                    
                    isPeakTime = json.getBoolean("isPeakTime");
                    isPremium = Pattern.matches("[\\S\\s]*var User =.*?isPremium:\\s*true,[\\S\\s]*", content);
                }
            }
            if (videoId != null) {
                if (isDmc && videos != null && audios != null) {
                    boolean economy = isDmcEconomy();
                    // NicoCachingTitleRetriever.putTitleCacheを通っているので
                    // エントリは必ず存在する
                    NicoIdInfoCache.Entry info =
                            NicoIdInfoCache.getInstance().get(videoId.substring(2));
                    if (qualityVideos != null && qualityAudios != null) {
                        boolean low;
                        low = false;
                        for (Map.Entry<String, Boolean> e : qualityVideos.entrySet()) {
                            boolean kuLocked = isKuLockedVideo(e.getKey());
                            info.dmcVideoEconomy.put(e.getKey(), low);
                            info.dmcVideoKuLocked.put(e.getKey(), kuLocked);
                            if (kuLocked) {
                                info.setKuLocked(true);
                            } else {
                                low = true;
                            }
                        }
                        low = false;
                        for (Map.Entry<String, Boolean> e : qualityAudios.entrySet()) {
                            boolean kuLocked = isKuLockedAudio(e.getKey());
                            info.dmcAudioEconomy.put(e.getKey(), low);
                            info.dmcAudioKuLocked.put(e.getKey(), kuLocked);
                            if (kuLocked) {
                                info.setKuLocked(true);
                            } else {
                                low = true;
                            }
                        }
                    } else {
                        for (int i = 0; i < videos.size(); i++) {
                            info.dmcVideoEconomy.put(videos.get(i), i != 0 || economy);
                        }
                        for (int i = 0; i < audios.size(); i++) {
                            info.dmcAudioEconomy.put(audios.get(i), i != 0 || economy);
                        }
                    }
                }
                cache.put(videoId, this);
            }
        } else {
            Logger.warning("WatchVars: JSON not found.");
        }
        if (Boolean.getBoolean("debugWatchVars")) {
            Logger.info("WatchVars: JSON " + this.getJSON());
            Logger.info("WatchVars: INFO " + videoId + " "
                    + (ginza ? "GINZA " : "")
                    + (harajuku ? "Harajuku " : "")
                    + (isDmc ? "dmc" : "classic") + " " + videoTitle);
            Logger.info("WatchVars: INFO2"
                    + (isPremium ? " Premium" : " !Premium")
                    + (isPeakTime ? " PeakTime" : " !PeakTime"));
            if (isDmc) {
                NicoIdInfoCache.Entry info =
                        NicoIdInfoCache.getInstance().get(videoId.substring(2));
                if (qualityVideos != null) {
                    Logger.info("WatchVars: VIDEOS (all)");
                    for (Map.Entry<String, Boolean> e : qualityVideos.entrySet()) {
                        Boolean low = info == null ? null : info.dmcVideoEconomy.get(e.getKey());
                        Logger.info("WatchVars: - " + (e.getValue() ? "" : "!") + e.getKey()
                                + (low == null ? " null" : low ? " low" : "")
                                + (isKuLockedVideo(e.getKey()) ? " ku" : ""));
                    }
                } else if (videos != null) {
                    Logger.info("WatchVars: VIDEOS (availables)");
                    for (String video : videos) {
                        Boolean low = info == null ? null : info.dmcVideoEconomy.get(video);
                        Logger.info("WatchVars: - " + video
                                + (low == null ? " null" : low ? " low" : ""));
                    }
                }
                if (qualityAudios != null) {
                    Logger.info("WatchVars: AUDIOS (all)");
                    for (Map.Entry<String, Boolean> e : qualityAudios.entrySet()) {
                        Boolean low = info == null ? null : info.dmcAudioEconomy.get(e.getKey());
                        Logger.info("WatchVars: - " + (e.getValue() ? "" : "!") + e.getKey()
                                + (low == null ? " null" : low ? " low" : "")
                                + (isKuLockedAudio(e.getKey()) ? " ku" : ""));
                    }
                } else if (audios != null) {
                    Logger.info("WatchVars: AUDIOS (availables)");
                    for (String audio : audios) {
                        Boolean low = info == null ? null : info.dmcAudioEconomy.get(audio);
                        Logger.info("WatchVars: - " + audio
                                + (low == null ? " null" : low ? " low" : ""));
                    }
                }
                Logger.info("WatchVars: INFO3"
                        + (isDmcEconomy() ? " DmcEconomy" : " !DmcEconomy"));
            }
            StringBuilder sb = new StringBuilder("WatchVars: TAGS");
            for (String tag : getTags()) sb.append(" ").append(tag);
            Logger.info(sb.toString());
        }
    }

    // 原宿の watch ページを JSON にでっち上げる(watchAPI のプレイリスト形式)
    private static JsonObject harajuku2json(String contents) {
        boolean found = false;
        StringBuilder sb = new StringBuilder("{");
        
        Matcher m = SO_ADDVARIABLE_PATTERN.matcher(contents);
        while (m.find()) {
            found = true;
            if (sb.length() > 1) {
                sb.append(",");
            }
            sb.append(m.group(1)).append(":");
            
            // 厳密にやるならメンバによって文字列と数値を使い分ける必要あり
            String s = TextUtil.escapeJSON(decodeURL(m.group(2)));
            sb.append('"').append(s).append('"');
        }
        if (!found) {
            return null;
        }
        if ((m = VAR_VIDEO_PATTERN.matcher(contents)).find()) {
            // JavaScript から JSON に変換
            String s = m.group(1)
                    .replaceAll("\\s*//[^\"\n]+", "")
                    .replaceAll("\\s+(\\w+):\\s+", "\"$1\":")
                    .replaceAll("\\s*\\[\\s+([\\S]+?)\\s+\\]", "[$1]")
                    .replaceAll("'", "\"");
            sb.append(",\"videoDetail\":").append(s);
        }
        
        return Json.parseObject(sb.append("}").toString());
    }

    private static final Pattern VIDEO_MODE_PATTERN = Pattern.compile("^(\\d+)([pi])$");
    private static final Pattern VIDEO_TYPE_PATTERN = Pattern.compile("^archive_(?:[^_]+)_(\\d+)kbps_([^_]+)$");
    private static final Pattern AUDIO_TYPE_PATTERN = Pattern.compile("^archive_(?:[^_]+)_(\\d+)kbps$");
    
    /**
     * dmcのエコノミー判定(ヒューリスティック)
     */
    private boolean isDmcEconomy() {
        if (isPremium || !isPeakTime) {
            return false;
        }
        if (videos == null || videos.size() == 0 ||
                audios == null || audios.size() == 0) {
            return false;
        }
        String video = videos.get(0);
        String audio = audios.get(0);
        int videoBitrate = Integer.MAX_VALUE;
        int audioBitrate = Integer.MAX_VALUE;
        Matcher mv = VIDEO_TYPE_PATTERN.matcher(video);
        if (mv.matches()) {
            videoBitrate = Integer.parseInt(mv.group(1));
        }
        Matcher ma = AUDIO_TYPE_PATTERN.matcher(audio);
        if (ma.matches()) {
            audioBitrate = Integer.parseInt(ma.group(1));
        }
        return videoBitrate <= 600 && audioBitrate <= 64;
    }
    
    private boolean isKuLockedVideo(String video) {
        int bitrate = Integer.MAX_VALUE;
        String mode;
        
        Matcher m1 = VIDEO_TYPE_PATTERN.matcher(video);
        if (!m1.matches()) return false;
        
        bitrate = Integer.parseInt(m1.group(1));
        mode = m1.group(2);
        
        Matcher m2 = VIDEO_MODE_PATTERN.matcher(mode);
        if (!m2.matches()) return false;
        
        int height = Integer.parseInt(m2.group(1));
        
        if (duration < 16 * 60) {
            if (height <= 540) return bitrate > 1000;
            if (height <= 720) return bitrate > 2000;
            return true;
        } else if (duration < 31 * 60) {
            if (height <= 540) return bitrate > 1000;
            return true;
        } else if (duration < 61 * 60) {
            if (height <= 360) return bitrate > 600;
            return true;
        } else {
            if (height <= 360) return bitrate > 300;
            return true;
        }
    }
    
    private boolean isKuLockedAudio(String audio) {
        int bitrate = Integer.MAX_VALUE;
        
        Matcher m = AUDIO_TYPE_PATTERN.matcher(audio);
        if (!m.matches()) return false;
        
        bitrate = Integer.parseInt(m.group(1));
        
        return bitrate > 192;
    }
    
    private Map<String, Boolean> getQualityAvailability(JsonArray jsonQuality) {
        if (jsonQuality == null) {
            return null;
        }
        Map<String, Boolean> result = new LinkedHashMap<>();
        for (JsonValue o : jsonQuality.getList()) {
            if (o instanceof JsonObject) {
                JsonObject obj = (JsonObject)o;
                String id = obj.getString("id");
                Boolean available = obj.getBoolean("available");
                if (id != null && available != null) {
                    result.put(id, available);
                }
            }
        }
        return result;
    }
    
    private static String decodeURL(String value) {
        try {
            return URLDecoder.decode(value, "UTF-8");
        } catch (IllegalArgumentException e) {
            Logger.debug(e);
            return value;
        } catch (UnsupportedEncodingException e) {
            Logger.error(e);
        }
        return value;
    }
    
    private static String encodeURL(String value) {
        try {
            return URLEncoder.encode(value, "UTF-8");
        } catch (IllegalArgumentException e) {
            Logger.debug(e);
            return value;
        } catch (UnsupportedEncodingException e) {
            Logger.error(e);
        }
        return value;
    }
    
    /**
     * パースした JSON を再構築して返す
     * @return 再構築した JSON、パースできなかった場合は null
     */
    public String getJSON() {
        if (json != null) {
            return json.toJson();
        }
        return null;
    }
    
    /**
     * パースした JSON をオブジェクトで返す
     * @return 再構築した JSON、パースできなかった場合は null
     */
    public JsonObject getJsonObject() {
        return json;
    }
    
    /**
     * 動画IDを取得する
     * @return 動画ID、取得できない場合は null
     */
    public String getVideoId() {
        return videoId;
    }
    
    /**
     * 動画タイトルを取得する
     * @return 動画タイトル、取得できない場合は null
     */
    public String getVideoTitle() {
        return videoTitle;
    }
    
    /**
     * プレミアムでアクセスしているかを取得する
     * @return プレミアムなら true
     */
    public boolean isPremium() {
        return isPremium;
    }
    
    /**
     * 低画質になる時間帯にアクセスしているかを取得する
     * @return 低画質時間帯なら true
     */
    public boolean isPeakTime() {
        return isPeakTime;
    }
    
    /**
     * 動画が新形式かどうかを取得する
     * @return 新形式なら true
     */
    public boolean isDmc() {
        return isDmc;
    }
    
    /**
     * パースした dmcInfo をオブジェクトで返す
     * @return dmcInfo、パースできなかった場合は null
     */
    public JsonObject getDmcInfo() {
        return dmcInfo;
    }
    
    /**
     * 動画タグを取得する
     * @return 動画タグのリスト、取得できない場合は空リスト
     */
    public List<String> getTags() {
        ArrayList<String> tags = new ArrayList<String>();
        if (json != null) {
            JsonValue v;
            if (ginza) {
                v = json.get("videoDetail", "tagList");
            } else {
                v = json.get("tags");
            }
            if (v instanceof JsonArray) {
                for (JsonValue vv : ((JsonArray) v).getList()) {
                    String tag;
                    if (ginza) {
                        tag = vv.getString("tag");
                    } else {
                        tag = vv.getString("name");
                    }
                    if (tag != null) tags.add(tag);
                }
            }
        }
        return tags;
    }
    
    static HttpRequestHeader  REQUEST_HEADER_RIAPI;
    static HttpResponseHeader RESPONSE_HEADER_HTML;
    static HttpResponseHeader RESPONSE_HEADER_JSON;
    static HttpResponseHeader RESPONSE_HEADER_PLAIN;
    static {
        try {
            REQUEST_HEADER_RIAPI = newRequestHeader(
                    "http://riapi.nicovideo.jp/api/");
            RESPONSE_HEADER_HTML = new HttpResponseHeader(
                    "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n");
            RESPONSE_HEADER_JSON = new HttpResponseHeader(
                    "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n");
            RESPONSE_HEADER_PLAIN = new HttpResponseHeader(
                    "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n");
        } catch (IOException e) {
            Logger.error(e);
        }
    }
    
    private static HttpRequestHeader newRequestHeader(String url) {
        HttpRequestHeader requestHeader = null;
        try {
            requestHeader = new HttpRequestHeader(
                    HttpHeader.GET + " " + url + " HTTP/1.1\r\n\r\n");
            requestHeader.setMessageHeader("User-Agent",
                    TextUtil.escapeHTML(Main.VER_STRING));
        } catch (IOException e) {
            Logger.error(e);
        }
        return requestHeader;
    }
    
    /**
     * コンストラクタに渡した watch ページの内容を取得する
     * @return watch ページの内容
     */
    public String getContent() {
        return content;
    }
    
    /**
     * watch ページ内の JSON に対して nlFilter で置換した内容を取得する
     * <p>
     * まず JSON 全体に対して nlFilter を適用した上で、更に JSON
     * データ部分の特定の値に対してエスケープを解除してから nlFilter
     * を適用し、再エスケープ処理を行う。
     * <p>
     * 対象となる JSON は以下の通り
     * <ul>
     * <li>watchAPI として取得した JSON
     * <li>watch ページの watchAPIDataContainer 内の JSON
     * </ul>
     * <p>
     * 置換対象のデータ部分は以下の通り
     * <ul>
     * <li>videoDetail 内の description の値
     * </ul>
     * 
     * @return JSON 内部を置換した watch ページの内容、
     * JSON を含まない場合は {@linkplain #getContent()} と同じ
     */
    public String getReplacedContent() {
        if (!harajuku && videoId != null) {
            if (requestHeader == null) {
                requestHeader = newRequestHeader(
                        "http://www.nicovideo.jp/watch/" + videoId);
            }
            if (ginza) {
                replaceJSON("videoDetail", "description");
            } else {
                replaceJSON("video", "description");
            }
            
            String replaced = addCacheIcon(json.toJson());
            if (content != data && !rawJson) {
                StringBuilder sb = new StringBuilder(content);
                sb.replace(dataMatcher.start(1), dataMatcher.end(1),
                        TextUtil.escapeHTML(replaced));
                replaced = replacePlaylistData(sb);
            }
            return replaced;
        }
        return content;
    }
    
    // nlFilter を使って playlistData を置換
    private String replacePlaylistData(StringBuilder sb) {
        String replaced = sb.toString();
        Matcher m = PLAYLIST_DATA_CONTAINER_PATTERN.matcher(replaced);
        if (m.find()) {
            String playlistData = TextUtil.unescapeHTML(m.group(1));
            sb.replace(m.start(1), m.end(1),
                    TextUtil.escapeHTML(addCacheIcon(playlistData)));
            replaced = sb.toString();
        }
        return replaced;
    }
    
    // nlFilter を使って JSON 内の値を置換
    private void replaceJSON(String...args) {
        JsonValue v = this.getValue(args);
        if (v instanceof JsonString) {
            String s = v.toString();
            s = EasyRewriter.replace(null, s,
                    requestHeader, RESPONSE_HEADER_HTML);
            ((JsonString) v).setValue(s);
        }
    }
    
    // nlFilter を使ってキャッシュ情報を付加
    private String addCacheIcon(String data) {
        return EasyRewriter.replace(null, data,
                REQUEST_HEADER_RIAPI, RESPONSE_HEADER_JSON);
    }
    
    
    // WatchRewriterからの呼び出し用(flvInfoにgetflvの置換を適用)
    void replaceFlvInfo(String watch_url) {
        if (!ginza || videoId == null || flashvars == null) {
            return;
        }
        JsonValue v = flashvars.get("flvInfo");
        if (v instanceof JsonString) {
            String flvInfo = decodeURL(v.toString());
Logger.debug("original flvInfo: " + flvInfo);
            Matcher m = WATCH_ECONOMY_MODE_PATTERN.matcher(watch_url);
            String url_param = "/" + videoId + (m.find() ? m.group() : "");
            HttpRequestHeader getflvRequestHeader = newRequestHeader(
                    "http://flapi.nicovideo.jp/api/getflv" + url_param);
            flvInfo = EasyRewriter.replace(null,
                    GetFlvRewriter.replace(url_param, flvInfo),
                    getflvRequestHeader, RESPONSE_HEADER_PLAIN);
Logger.debug("replaced flvInfo: " + flvInfo);
            ((JsonString) v).setValue(encodeURL(flvInfo));
        }
    }

    private static final Pattern SM_FLV_PATTERN =
            Pattern.compile("^http://(?!tn-skr)[^/]+(?:smilevideo|nicovideo)\\.jp/smile\\?(\\w)=([^.]+)\\.\\d+(as3)?(low)?$");
    void replaceDataApiData(String watch_url) {
        if (ginza || json == null || json.get("video") == null) {
            return;
        }

        String smid = getVideoId();
        
// lowのキャッシュがブラウザに残ってた時の対策
// eco=1で通常キャッシュがあるときはそのままにして、後で利用する
        Matcher m = WATCH_ECONOMY_MODE_PATTERN.matcher(watch_url);
        if (!m.find()) {
            String videoSource = json.getString("video", "smileInfo", "url");
            if (videoSource == null) {
                return;
            }
            
            String postfix = ".unknown";
            Matcher m_url = SM_FLV_PATTERN.matcher(videoSource);
            if (m_url.matches()) {
                String format = m_url.group(1);
                if (format.equals("v")) {
                    postfix = ".flv";
                } else if (format.equals("m")) {
                    postfix = ".mp4";
                } else if (format.equals("s")) {
                    postfix = ".swf";
                }
                
                // smidと動画IDが異なる場合にsmidでキャッシュするために記憶しておく
                String vid = m_url.group(2);
                String cid = videoId.substring(2);
                if (!vid.equals(cid) && !json.getBoolean("video", "isDeleted")) {
                    if (NLConfig.matches("deletedVideoId", vid)) {
                        // 以降は削除された動画として処理
                        json.getObject("video").put("isDeleted", new JsonTrue());
                    } else {
                        NLShared.INSTANCE.put_vid2cid(vid, cid);
                    }
                }
            }
            
            VideoDescriptor cachedVideo = Cache.getPreferredCachedVideo(smid, false, null);
            if (cachedVideo == null) { // not exists
                return;
            }
            Cache cache = new Cache(cachedVideo);
            if (videoSource.endsWith("low") && !cache.isLow()
                    && (Boolean.getBoolean("convertFlv2Mp4") || postfix.equals(cache.getPostfix()))) {
                String newSource = videoSource.substring(0, videoSource.length() - 3);
                Json.putString(newSource, json, "video", "smileInfo", "url");
            }
        }
    }
    
    /**
     * watch ページ内の JSON から値を取得して文字列で返す
     * <p>
     * Zeroの場合は watchAPIDataContainer 内の JSON が対象となる。
     * 原宿の場合は so.addVariable の値を元に構築し、更に var Video の値を
     * videoDetail オブジェクトにマップした JSON が対象となる（プレイリスト形式）。
     * 原宿の場合は内部で擬似的に JSON にマップしているため以下の違いがある。
     * <ul>
     * <li>Zero では flashvars オブジェクト内の値が JSON オブジェクト直下にある
     * <li>Zero ではタグの扱いが tagList だが、原宿では tags/lockedTags となる
     * <li>Zero に存在する uploaderInfo と viewerInfo は無い
     * <li>数値や真偽値の扱いがリテラルの場合と文字列の場合がある
     * </ul>
     * <p>
     * JSON は階層構造なので、引数には取得対象の値を特定するための
     * メンバー文字列を階層構造順に複数指定する（そのため可変引数となる）。
     * 例えば、VideoDetail 内の description の値を取得したい場合、
     * {@code getString("VideoDetail", "description")} と呼び出す。
     * <p>
     * 引数を一つだけ指定した場合、その値が存在しない場合は flashvars から再取得する。
     * そのため、原宿にも対応するには flashvars を引数で直接指定しない方が良い。
     * 
     * @param args 取得対象の値を特定するメンバー文字列
     * @return 取得対象の文字列、存在しない or 値自体がヌルの場合は null
     */
    @Deprecated
    public String getString(String...args) {
        if (json != null) {
            JsonValue v = getValue(args);
            if (v instanceof JsonNull) {
                return null;
            } else if (v != null) {
                return v.toString();
            }
        }
        return null;
    }
    
    private JsonValue getValue(String...args) {
        JsonValue v = json.get((Object[])args);
        if (v == null && args.length == 1 && flashvars != null) {
            return flashvars.get(args[0]);
        }
        return v;
    }
    
    /**
     * watch ページ内の JSON から値を取得して真偽値で返す
     * @param args 取得対象の値を特定するメンバー文字列
     * @return 取得対象の値が存在し真と評価されるときに true
     * @see #getString(String...)
     */
    @Deprecated
    public boolean getBoolean(String...args) {
        if (json != null) {
            JsonValue v = getValue(args);
            if (v != null) {
                return v.toBoolean();
            }
        }
        return false;
    }
    
    /**
     * watch ページ内の JSON から値を取得して整数値(int)で返す
     * @param args 取得対象の値を特定するメンバー文字列
     * @return 取得対象の整数値、存在しない or 値が不正 or 値自体がゼロの場合は 0
     * @see #getString(String...)
     */
    @Deprecated
    public int getInteger(String...args) {
        long l = getLong(args);
        return (int)l;
    }
    
    /**
     * watch ページ内の JSON から値を取得して整数値(long)で返す
     * @param args 取得対象の値を特定するメンバー文字列
     * @return 取得対象の整数値、存在しない or 値が不正 or 値自体がゼロの場合は 0
     * @see #getString(String...)
     */
    @Deprecated
    public long getLong(String...args) {
        if (json != null) {
            JsonValue v = getValue(args);
            if (v instanceof JsonNumber) {
                return ((JsonNumber) v).getLong();
            } else if (v instanceof JsonString) {
                try {
                    return Long.parseLong(((JsonString) v).value());
                } catch (NumberFormatException e) {
                    Logger.debug(e);
                }
            }
        }
        return 0;
    }
    
    /**
     * watch ページ内の JSON から値を取得して実数値(double)で返す
     * @param args 取得対象の値を特定するメンバー文字列
     * @return 取得対象の実数値、存在しない or 値が不正 or 値自体がゼロの場合は 0
     * @see #getString(String...)
     */
    @Deprecated
    public double getDouble(String...args) {
        if (json != null) {
            JsonValue v = getValue(args);
            if (v instanceof JsonNumber) {
                return ((JsonNumber) v).getDouble();
            } else if (v instanceof JsonString) {
                try {
                    return Double.parseDouble(((JsonString) v).value());
                } catch (NumberFormatException e) {
                    Logger.debug(e);
                }
            }
        }
        return 0;
    }
    
    private static List<String> getArrayOfString(JsonArray a) {
        if (a != null) {
            List<JsonValue> al = a.getList();
            ArrayList<String> result = new ArrayList<String>(al.size());
            for (JsonValue v2 : al) {
                if (v2 instanceof JsonString) {
                    result.add(v2.toString());
                } else {
                    return null;
                }
            }
            return result;
        }
        return null;
    }
    
    // テスト用
    public static void main(String args[]) {
        for (int i = 0; i < args.length; i++) {
            FileReader fr = null;
            StringBuilder sb = new StringBuilder();
            try {
                fr = new FileReader(args[i]);
                int c;
                while ((c = fr.read()) != -1) {
                    sb.append((char) c);
                }
                fr.close();
                String input = TextUtil.unescapeHTML(sb.toString());
                System.out.println("INPUT = " + input);
                JsonValue json = Json.parse(input);
                if (json != null) {
                    System.out.println("PARSE = " + json.toJson());
                }
            } catch (IOException e) {
                System.out.println(e.toString());
            }
        }
    }
    
}
