package extensions;

import dareka.common.Logger;
import dareka.common.Pair;
import dareka.extensions.Extension2;
import dareka.extensions.ExtensionManager;
import dareka.processor.HttpRequestHeader;
import dareka.processor.HttpResponseHeader;
import dareka.processor.Processor;
import dareka.processor.Resource;
import dareka.processor.StringResource;
import dareka.processor.URLResource;
import dareka.processor.impl.Cache;
import dareka.processor.impl.ReEncodingInfo;
import dareka.processor.impl.VideoDescriptor;
import dareka.processor.util.GetThumbInfoUtil;
import java.io.IOException;
import java.net.Socket;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.util.Deque;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class reEncodingChecker implements Extension2, Processor {

    public static final int REVISION = 170207;
    public static final String VER_STRING = "reEncodingChecker_" + REVISION;
    public static final String LOG_PREFIX = "reEncodingChecker";
    public static final String PROP_DEBUG = "reEncodingCheckerDebug";

    private static final String[] PROCESSOR_SUPPORTED_METHODS = new String[]{"GET"};
    private static final Pattern PROCESSOR_SUPPORTED_PATTERN
            = Pattern.compile("^http://\\w+\\.nicovideo.jp/cache/reEncodingChecker\\?(.*)");
    private static final Pattern WATCHID_PATTERN = Pattern.compile("^(\\w{2}\\d+)$");
    
    private final String STATUS_OK = "OK";
    private final String STATUS_ERROR = "error";
    private final String STATUS_UNAUTHORIZED = "unauthorized";
    private final String STATUS_ECONOMY = "economy";
    private final String STATUS_NONMP4 = "non-mp4";
    private final String STATUS_TOO_FREQUENT = "too frequent remote access";
    
    public reEncodingChecker() {
        
    }
    
    // logger wrapper
    protected static void warning(String message) {
        Logger.warning(LOG_PREFIX + ": " + message);
    }
    protected static void info(String message) {
        Logger.info(LOG_PREFIX + ": " + message);
    }
    protected static void debug(String message) {
        if (Boolean.getBoolean(PROP_DEBUG)) {
            Logger.info(LOG_PREFIX + ": " + message);
        }
    }

    // Extension2 interface
    @Override
    public void registerExtensions(ExtensionManager mgr) {
        mgr.registerProcessor(this);
    }

    @Override
    public String getVersionString() {
        return VER_STRING;
    }
    
    // Processor interface
    @Override
    public String[] getSupportedMethods() {
        return PROCESSOR_SUPPORTED_METHODS;
    }

    @Override
    public Pattern getSupportedURLAsPattern() {
        return PROCESSOR_SUPPORTED_PATTERN;
    }

    @Override
    public String getSupportedURLAsString() {
        return null;
    }

    @Override
    public Resource onRequest(HttpRequestHeader requestHeader, Socket browser) throws IOException {
        String uri = requestHeader.getURI();
        Matcher m = PROCESSOR_SUPPORTED_PATTERN.matcher(uri);
        if (!m.find()) {
            // it must not happen...
            return null;
        }
        String watchId = m.group(1);
        m = WATCHID_PATTERN.matcher(watchId);
        if (!m.find()) {
            return StringResource.getNotFound();
        }

        String videoId = watchId2VideoId(watchId);
        
        // まず判定結果が記録済みならそれを返す
        Boolean reencoded = ReEncodingInfo.get(videoId);
        if (reencoded != null) {
            return respond(STATUS_OK, reencoded);
        }
        // キャッシュからの確認を試みる
        Pair<String, Boolean> ret = checkIfCached(videoId);
        if (ret == null) {
            // キャッシュで分からなければ動画の先頭を取りに行く
            ret = requestToCheck(requestHeader, watchId);
        }
        return respond(ret.first, ret.second);
    }
    
    // id2smidとの違い: id2smidはwatchページから取得
    private String watchId2VideoId(String watchId) {
        String videoId;
        if (watchId.matches("\\d+")) {
            videoId = Cache.id2Smid(watchId);
            debug("id2Smid " + watchId + " -> " + videoId);
            if (videoId == null) {
                // threadidとsmidの対応がキャッシュされていない場合は
                // ThumbInfoに取りに行く
                try {
                    String thumbInfo = GetThumbInfoUtil.get(watchId);
                    videoId = GetThumbInfoUtil.getSmid(thumbInfo);
                } catch (IOException e) {
                }
                debug("GetThumbInfoUtil.getSmid " + videoId);
                // それでも取得できない場合はローカルでの確認を諦める
                if (videoId == null) {
                    return null;
                }
            }
        } else {
            videoId = watchId;
        }
        return videoId;
    }
    
    private Pair<String, Boolean> checkIfCached(String videoId) {
        // キャッシュから判定
        VideoDescriptor video = VideoDescriptor.newClassic(videoId, Cache.MP4, false);
        Cache cache = new Cache(video);
        // まず判定する．結果が不明の場合は原因を振り分ける．
        Boolean reencoded = cache.isReEncodedStrictly();
        if (reencoded != null) {
            return new Pair<>(STATUS_OK, reencoded);
        }
        // キャッシュがなければサーバへ取りに行く
        if (!cache.exists()) {
            return null;
        }
        // 動画の形式がMP4ではない場合
        if (!cache.getCacheFileName().endsWith(Cache.MP4)) {
            return new Pair<>(STATUS_NONMP4, null);
        }
        // キャッシュが変
        return new Pair<>(STATUS_ERROR, null);
    }
    
    private Pair<String, Boolean> requestToCheck(HttpRequestHeader requestHeader, String watchId) {
        String status = limitRemoteAccess();
        if (status != null) {
            return new Pair<>(status, null);
        }
        
        info("remote checking " + watchId);
        String videoId = watchId;
        try {
            URLResource r = getWatchPageResource(requestHeader, watchId);
            if (r == null) {
                debug("r == null");
                return new Pair<>(STATUS_ERROR, null);
            }
            HttpResponseHeader responseHeader = r.getResponseHeader(null, null);
            byte[] body = r.getResponseBody();
            if (responseHeader != null && body != null) {
                String content = new String(body, ExtUtil.getCharset(responseHeader));
                if (responseHeader.getStatusCode() != 200) {
                    debug("responseHeader.getStatusCode() != 200");
                    return new Pair<>(STATUS_ERROR, null);
                }

                String authflag = responseHeader.getMessageHeader("x-niconico-authflag");
                if (authflag == null || authflag.equals("0")) {
                    return new Pair<>(STATUS_UNAUTHORIZED, null);
                }
                if (watchId.matches("\\d+")) {
                    // コミュニティの場合はgetthumbinfoで取得出来ないので
                    // 改めてwatchページから取得する
                    videoId = id2smid(watchId, content);
                }
                if (!setNicohistoryCookie(requestHeader, responseHeader)) {
                    debug("!setNicohistoryCookie(requestHeader, responseHeader)");
                    return new Pair<>(STATUS_ERROR, null);
                }
                String url = extractVideoUrlFromWatchPage(content);
                status = validateVideoUrl(videoId, url);
                if (status != null) {
                    return new Pair<>(status, null);
                }
                debug("fetch: " + url);
                byte[] videoHeader = fetch(requestHeader, url);
                if (videoHeader == null) {
                    debug("videoHeader == null");
                    return new Pair<>(STATUS_ERROR, null);
                }
                Boolean result = ReEncodingInfo.check(videoId, ByteBuffer.wrap(videoHeader));
                if (result == null) {
                    debug("ReEncodingInfo.check(videoId, ByteBuffer.wrap(videoHeader)) == null");
                    return new Pair<>(STATUS_ERROR, null);
                }
                return new Pair<>(STATUS_OK, result);
            }
        } catch (IOException e) {
            Logger.error(e);
        }
        return new Pair<>(STATUS_ERROR, null);
    }
    
    private final Deque<Long> limitQueue = new LinkedList<>();
    
    private synchronized String limitRemoteAccess() {
        long now = System.currentTimeMillis();
        long period = Integer.getInteger("reEncodingCheckerLimit", 20);
        if (period < 10) period = 10;
        // consume expired entires
        while (!limitQueue.isEmpty()) {
            long x = limitQueue.peek();
            if (x <= now) {
                limitQueue.remove();
            } else {
                break;
            }
        }
        // 3回まで連続アクセスを許可
        if (limitQueue.size() >= 3) {
            return STATUS_TOO_FREQUENT;
        }
        // 初回は現在時刻の period 秒後
        // 2回目以降は period 秒ごとに並ぶ
        if (limitQueue.isEmpty()) {
            limitQueue.add(now + period * 1000);
        } else {
            limitQueue.add(limitQueue.peekLast() + period * 1000);
        }
        return null;
    }
    
    private static final Pattern GINZA_URL_PATTERN = Pattern.compile(
            "&quot;flvInfo&quot;:&quot;[^&]*?%26url%3D(.*?)%26");
    private static final Pattern HTML5_URL_PATTERN = Pattern.compile(
            "&quot;video&quot;:\\{[^}]*?&quot;source&quot;:&quot;(http:.*?)&quot;");
    private String extractVideoUrlFromWatchPage(String content) {
        Matcher m;
        m = HTML5_URL_PATTERN.matcher(content);
        if (m.find()) {
            return m.group(1).replace("\\", "");
        }
        m = GINZA_URL_PATTERN.matcher(content);
        if (m.find()) {
            try {
                return URLDecoder.decode(URLDecoder.decode(m.group(1), "UTF-8"), "UTF-8");
            } catch (Exception e) {
                Logger.error(e);
                return null;
            }
        }
        return null;
    }
    
    private static final Pattern SM_FLV_PATTERN =
            Pattern.compile("^http://(?!tn-skr)[^/]+(?:smilevideo|nicovideo)\\.jp/smile\\?(\\w)=([^.]+)\\.\\d+(as3)?(low)?$");
    private String validateVideoUrl(String videoId, String url) {
        Matcher m = SM_FLV_PATTERN.matcher(url);
        if (!m.matches()) {
            return STATUS_ERROR;
        }
        if (!m.group(1).equals("m") || m.group(3) != null) {
            return STATUS_NONMP4;
        }
        if (m.group(4) != null) {
            return STATUS_ECONOMY;
        }
        if (!m.group(2).equals(videoId.substring(2))) {
            return STATUS_ERROR;
        }
        return null;
    }
    
    private byte[] fetch(HttpRequestHeader requestHeader, String url) {
        try {
            URLResource r = new URLResource(url);
            ExtUtil.setURL(requestHeader, url, true);
            requestHeader.setMessageHeader("Range", "bytes=0-511");
            HttpResponseHeader responseHeader =
                    r.getResponseHeader(null, requestHeader);
            if (responseHeader != null) {
                int statusCode = responseHeader.getStatusCode();
                if (statusCode == 206) {
                    byte[] result = r.getResponseBody();
                    if (result.length != 512) {
                        return null;
                    } else {
                        return result;
                    }
                } else {
                    debug("statusCode: " + statusCode);
                }
            }
        } catch (IOException e) {
            Logger.error(e);
        } finally {
            requestHeader.removeMessageHeader("Range");
        }
        return null;
    }
    
    private Resource respond(String status, Boolean reencoded) {
        StringBuilder sb = new StringBuilder();
        sb.append('{');
        putKeyValue(sb, "status", status);
        if (reencoded != null) {
            putKeyValue(sb, "reencoded", reencoded);
        }
        sb.replace(sb.length() - 1, sb.length(), "}");
        return new StringResource(sb.toString());
    }
    
    
    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, String value) {
        if (value == null) {
            return sb.append("null");
        } else {
            return sb.append('"')
                    .append(value.replace("\\", "\\\\").replace("\"", "\\\""))
                    .append('"');
        }
    }
    
    private void putKeyValue(StringBuilder sb, String key, boolean value) {
        putValue(putKey(sb, key), value).append(',');
    }
    
    private void putKeyValue(StringBuilder sb, String key, String value) {
        putValue(putKey(sb, key), value).append(',');
    }

    
    ////// from nlMovieFetcher //////
    private URLResource getWatchPageResource(HttpRequestHeader requestHeader, String videoId)
            throws IOException {
        String watch_url = "http://www.nicovideo.jp/watch/".concat(videoId);
        URLResource r = new URLResource(watch_url);
        r.setFollowRedirects(false); // リダイレクトを自前で処理する
        HttpResponseHeader responseHeader = r.getResponseHeader(
                null, ExtUtil.setURL(requestHeader, watch_url, false));
        if (responseHeader != null && ( (responseHeader.getStatusCode() / 100) ) == 3) {
            String location = responseHeader.getMessageHeader("Location");
            if (location != null) {
                if (location.contains("/ppv_video/")) {
                    return null; // FAILURE
                }
                if (location.startsWith("/")) {
                    watch_url = "http://www.nicovideo.jp".concat(location);
                } else {
                    watch_url = location;
                }
                r = new URLResource(watch_url);
                responseHeader = r.getResponseHeader(
                        null, ExtUtil.setURL(requestHeader, watch_url, false));
            } else {
                return null; // FAILURE
            }
        }
        return r; // レスポンスヘッダ受信済
    }
    
    private static final Pattern NICOHISTORY_PATTERN = Pattern.compile(
            "Set-Cookie\\s*?:\\s*?(nicohistory=[^;]+?);"); /* 121015 FIX */
    private boolean setNicohistoryCookie(HttpRequestHeader requestHeader,
            HttpResponseHeader responseHeader) {
        Matcher m = NICOHISTORY_PATTERN.matcher(responseHeader.toString());
        if (m.find()) {
            String cookie = requestHeader.getMessageHeader("Cookie");
            if (cookie == null) {
                cookie = m.group(1);
            } else {
                if (cookie.indexOf("nicohistory=") < 0) {
                    cookie += "; " + m.group(1);
                } else {
                    cookie = cookie.replaceFirst("nicohistory=[^;]+", m.group(1));
                }
            }
            requestHeader.setMessageHeader("Cookie", cookie);
            return true;
        }
        info("cannot get nicohistory cookie.");
        debug(responseHeader.toString());
        return false;
    }

    private static final Pattern WATCHPAGE_VIDEOID_PATTERN = Pattern.compile(
			"(?:(?<!\\w)videoId|&quot;video&quot;:\\{[^}]+?(?<!\\w)id)(?:\", \"|&quot;:&quot;)([a-z]{2}\\d+)(?!\\w)");
	private static final Pattern THUMBINFO_VIDEOID_PATTERN = Pattern.compile(
			"<video_id>([a-z]{2}\\d+)</video_id>");
    private String id2smid(String id, String content) {
        String smid = id;
        // smidがthread_idの場合はvideo_idを取得
        if (id.matches("\\d+")) {
            if (content != null) { // watchページ
                Matcher m = WATCHPAGE_VIDEOID_PATTERN.matcher(content);
                if (m.find()) return m.group(1);
            }
            try {
                String thumbinfo = GetThumbInfoUtil.get(id);
                if (thumbinfo != null) {
                    Matcher m = THUMBINFO_VIDEOID_PATTERN.matcher(thumbinfo);
                    if(m.find()) return m.group(1);
                }
            } catch (IOException e) {}
        }
        return smid;
    }
}
