package dareka.processor.impl;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dareka.common.CloseUtil;
import dareka.common.Logger;
import dareka.extensions.SystemEventListener;
import dareka.processor.HttpHeader;
import dareka.processor.HttpRequestHeader;
import dareka.processor.LocalCacheFileResource;
import dareka.processor.Processor;
import dareka.processor.Resource;
import dareka.processor.URLResource;

public class DmcCachingProcessor implements Processor {
    
    private static final String[] PROCESSOR_SUPPORTED_METHODS = new String[]{"GET"};
    // サーバ名はpa03とかpb04とか
    private static final Pattern PROCESSOR_SUPPORTED_PATTERN
            = Pattern.compile("^http://\\w+\\.dmc\\.nico(?::\\d+)?/vod/ht2_nicovideo/nicovideo-(\\w+?)(\\d+)_(\\w+)"
                    + "\\?.*ht2_nicovideo=([^&]+)");
    private static final Pattern START_PATTERN = Pattern.compile("start=([^&]+)");

    static final Pattern RANGE_PATTERN = Pattern.compile("^bytes=(\\d+)-(\\d*)");
    static final Pattern VIDEO_TYPE_PATTERN = Pattern.compile("^archive_(?:[^_]+)_(\\d+)kbps_([^_]+)$");
    static final Pattern AUDIO_TYPE_PATTERN = Pattern.compile("^archive_(?:[^_]+)_(\\d+)kbps$");

    private Executor executor;
    
    public DmcCachingProcessor(Executor executor) {
        this.executor = executor;
    }
    
    @Override
    public String[] getSupportedMethods() {
        return PROCESSOR_SUPPORTED_METHODS;
    }

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

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


    public Resource onRequest(HttpRequestHeader requestHeader, Socket browser)
            throws IOException {
        NicoCachingProcessor.giantLock.lock();
        try {
            return onRequestCore(requestHeader, browser);
        } finally {
            NicoCachingProcessor.giantLock.unlock();
        }
    }
    protected Resource onRequestCore(HttpRequestHeader requestHeader, Socket browser)
            throws IOException {
        Matcher m = PROCESSOR_SUPPORTED_PATTERN.matcher(requestHeader.getURI());
        if (!m.find()) {
            // it must not happen...
            return Resource.get(Resource.Type.URL, requestHeader.getURI());
        }
        
        // start=0ではない時は素通しする
        {
            Matcher m2 = START_PATTERN.matcher(requestHeader.getURI());
            if (m2.find()) {
                if (!m2.group(1).equals("0")) {
                    Logger.info("Flash版でキャッシュ未完了位置へシークしたため，キャッシュ処理を継続できません．");
                    return Resource.get(Resource.Type.URL, requestHeader.getURI());
                }
            }
        }

        MovieData data;
        try {
            data = new MovieData(m);
        } catch (NoIdInfoException e) {
            Logger.info("idInfo is not found");
            return Resource.get(Resource.Type.URL, requestHeader.getURI());
        } catch (NoHt2EntryException e) {
            Logger.info("ht2 information is not found");
            return Resource.get(Resource.Type.URL, requestHeader.getURI());
        } catch (InvalidHt2EntryException e) {
            Logger.warning(e.getMessage());
            return Resource.get(Resource.Type.URL, requestHeader.getURI());
        }
        
        String smid = data.getSmid();
        VideoDescriptor video = data.getVideoDescriptor();
        String postfix = data.getPostfix();
        
        // [nl] Extensionにキャッシュ要求イベントを通知する
        NLEventSource eventSource = null;
        if (NLShared.INSTANCE.countSystemEventListeners() > 0) {
            eventSource = new NLEventSource(null, requestHeader, data.getCache());
            int result = NLShared.INSTANCE.notifySystemEvent(
                    SystemEventListener.CACHE_REQUEST, eventSource, true);
            if (result != SystemEventListener.RESULT_OK) {
                Logger.debug(requestHeader.getURI() + " pass-through by extension");
                return Resource.get(Resource.Type.URL, requestHeader.getURI());
            }
        }
        
        // 再エンコードされていない非dmcキャッシュを持っているときは応答する
        if (Boolean.getBoolean("useNotReEncodedCache")) {
            VideoDescriptor cachedVideo = CacheManager.getPreferredCachedVideo(smid, false, postfix);
            if (cachedVideo != null && !cachedVideo.isLow() && cachedVideo.getPostfix().equals(Cache.MP4)) {
                Cache cache = new Cache(cachedVideo);
                if (cache.exists() && !cache.isReEncoded()) {
                    Logger.info("using not-re-encoded cache: " + cache.getCacheFileName());
                    if (Boolean.getBoolean("touchCache")) {
                        cache.touch();
                    }
                    Resource r = new LocalCacheFileResource(cache.getCacheFile());
                    r.addCacheControlResponseHeaders(12960000);
                    r.setResponseHeader(HttpHeader.CONTENT_TYPE, "video/mp4");
                    r.setResponseHeader("Access-Control-Allow-Credentials", "true");
                    r.setResponseHeader("Access-Control-Allow-Origin", "http://www.nicovideo.jp");
                    
                    LimitFlvSpeedListener.addTo(r);
                    return r;
                }
            }
        }
        
        // 既にキャッシュを持っていたらキャッシュから応答する
        // TODO: この方法だとZenzaWatchなどで解像度切り替えが出来ないのでapiを乗っ取る
        VideoDescriptor cachedVideo = CacheManager.getPreferredCachedVideo(smid, true, postfix);
        Logger.debug("Preferred cache: " + cachedVideo);
        if (cachedVideo != null && !video.isPreferredThan(cachedVideo, true, null)) {
            Cache cache = new Cache(cachedVideo);
            if (cache.exists()) {
                if (!data.isLowAccess() && cache.isLow()) {
                    Logger.info("unmark low: " + cache.getCacheFileName());
                    cache.unmarkLow();
                }
                Logger.info("using cache: " + cache.getCacheFileName());
                if (Boolean.getBoolean("touchCache")) {
                    cache.touch();
                }
                Resource r = new LocalCacheFileResource(cache.getCacheFile());
                r.addCacheControlResponseHeaders(12960000);
                if (data.isFlash()) {
                    r.setResponseHeader(HttpHeader.CONTENT_TYPE, "video/flv");
                } else {
                    r.setResponseHeader(HttpHeader.CONTENT_TYPE, "video/mp4");
                }
                r.setResponseHeader("Access-Control-Allow-Credentials", "true");
                r.setResponseHeader("Access-Control-Allow-Origin", "http://www.nicovideo.jp");
                
                LimitFlvSpeedListener.addTo(r);
                return r;
            }
        }
        
        if (data.isFlash() && Boolean.getBoolean("disableCacheDmcFlv")) {
            return Resource.get(Resource.Type.URL, requestHeader.getURI());
        }
        
        // [nl] Extensionがキャッシュを禁止する場合はキャッシュしない
        if (eventSource != null) {
            int result = NLShared.INSTANCE.notifySystemEvent(
                    SystemEventListener.CACHE_STARTING, eventSource, true);
            if (result != SystemEventListener.RESULT_OK) {
                Logger.info("disable cache: " + data.getCache().getCacheFileName());
                return Resource.get(Resource.Type.URL, requestHeader.getURI());
            }
        }

        FutureTask<String> retrieveTitlteTask = null;
        if (Boolean.getBoolean("title")
                && (data.getIdInfo() == null || !data.getIdInfo().isTitleValid())) {
            retrieveTitlteTask =
                    new FutureTask<String>(new NicoCachingTitleRetriever(
                            data.getType(), data.getVideoId()));
            executor.execute(retrieveTitlteTask);
        }

        // [nl] ブラウザのレジューム対応と、NCでのレジューム対応
        requestHeader.removeMessageHeader("If-Range");
        long requestedPositionStart = -1;
        long requestedPositionEnd = -1;
        String range = requestHeader.getMessageHeader("Range");
        Logger.debug("Requested range: " + range);
        if (range != null) {
            Matcher e = RANGE_PATTERN.matcher(range);
            if (e.matches()) {
                requestedPositionStart = Long.parseLong(e.group(1));
                if (!e.group(2).equals("")) {
                    requestedPositionEnd = Long.parseLong(e.group(2));
                }
            }
        }
        
        MappedRanges mr = data.getCache().getCacheTmpMappedRanges();
        
        // キャッシュが完全でなくとも要求された部分は全部持っているならば
        // LocalCacheFileResourceで応答する(サーバにリクエストを発行しない)
        // DL中のセッションがいないのに全部取得済みでここに来た時は
        // 最後の1バイトだけサーバへリクエストさせてcompleterを走らせる．
        if (!Cache.getDLFlag(video) && mr != null && mr.isMapped(0, mr.getSize())) {
            long size = mr.getSize();
            mr.clear();
            mr.setSize(size);
            mr.map(0, size - 1);
        } else if (mr != null && mr.isMapped(requestedPositionStart, requestedPositionEnd)) {
            Resource r = new LocalCacheFileResource(data.getCache().getCacheTmpFile());
            Logger.info("using partial cache ["
                    + (requestedPositionStart == -1 ? "" : requestedPositionStart)
                    + "-"
                    + (requestedPositionEnd == -1 ? "" : requestedPositionEnd)
                    + "]: " + data.getCache().getCacheFileName());
            r.addCacheControlResponseHeaders(12960000);
            LimitFlvSpeedListener.addTo(r);
            return r;
        }

        Logger.info("no cache found: " + data.getCache().getCacheFileName());
        
        InputStream cacheInput = null;
        // [nl] レジュームするよー
        long initialPos = -1;
        if (mr != null) initialPos = mr.findTail(requestedPositionStart != -1 ? requestedPositionStart : 0);
        if (initialPos == -1) initialPos = requestedPositionStart != -1 ? requestedPositionStart : 0;
        if (initialPos != 0 && requestedPositionStart < initialPos) {
            String newRange;
            if (requestedPositionEnd != -1) {
                newRange = "bytes=" + initialPos + "-" + requestedPositionEnd;
            } else {
                newRange = "bytes=" + initialPos + "-";
            }
            Logger.debug("Rewriting range: " + newRange);
            requestHeader.setMessageHeader("Range", newRange);
            cacheInput =
                    new BufferedInputStream(data.getCache().getTmpInputStream());
            if (requestedPositionStart != -1) {
                cacheInput.skip(requestedPositionStart);
            }
        }

        URLResource r;
        try { // ensure cacheInput.close() in error cases.
            // [nl] DL中リストに入れる
            
            Cache.incrementDL(data.getCache().getVideoDescriptor());
            
            r = new URLResource(requestHeader.getURI());
            
            // *.dmc.nicoではないホストへリダイレクトされることがある．
            // 転送先は http://ISP内部のIPアドレス:80/data/16桁英数字/元のURLのホスト名以降 らしい．
            // CDNのようなものだろうか．
            r.setFollowRedirects(true);
            
            r.addTransferListener(new NicoCachingListener(data.getCache(),
                    retrieveTitlteTask, cacheInput, eventSource,
                    requestedPositionStart));
        } catch (RuntimeException e) {
            Logger.error(e);
            CloseUtil.close(cacheInput);
            Cache.decrementDL(data.getCache().getVideoDescriptor());
            throw e;
        }

        return r;
    }
    
    
    static class NoIdInfoException extends Exception { }
    static class NoHt2EntryException extends Exception { }
    static class InvalidHt2EntryException extends Exception {
        public InvalidHt2EntryException(String message) {
            super(message);
        }
    }

    /**
     * Class for manage various data for a movie.
     *
     */
    static class MovieData {
        private String smid;
        private String type;
        private String videoId;
        private String srcId;
        private String videoMode;
        private int videoBitrate, audioBitrate;
        private String postfix;
        private boolean economy;
        private String ht2Key;
        private Ht2Entry ht2Entry;
        private NicoIdInfoCache.Entry idInfo;
        private VideoDescriptor video;
        private Cache cache;

        MovieData(Matcher m) throws NoIdInfoException, NoHt2EntryException, InvalidHt2EntryException {
            initializeTypeIdHt2Key(m);
            initializeIdInfo(getVideoId());
            initializeHt2Entry(ht2Key);
            initializePostfix(ht2Entry);
            initializeModeAndBitrate(ht2Entry);
            initializeEconomy(ht2Entry, getIdInfo());
            initializeCache(getIdInfo());
        }

        private void initializeTypeIdHt2Key(Matcher m) {
            type = m.group(1);
            videoId = NLShared.INSTANCE.vid2cid(m.group(2));
            smid = type + videoId;
            srcId = m.group(3);
            ht2Key = m.group(4);
        }

        private void initializeIdInfo(String id) throws NoIdInfoException {
            idInfo = NicoIdInfoCache.getInstance().get(id);
            if (idInfo == null) {
                throw new NoIdInfoException();
            }
        }
        
        private void initializeHt2Entry(String ht2Key) throws NoHt2EntryException {
            ht2Entry = NLShared.INSTANCE.getHt2Manager().get(ht2Key);
            if (ht2Entry == null) {
                throw new NoHt2EntryException();
            }
        }

        private void initializePostfix(Ht2Entry ht2Entry) {
            if (ht2Entry.isFlash()) {
                postfix = Cache.FLV;
            } else {
                postfix = Cache.MP4;
            }
        }
        
        private void initializeModeAndBitrate(Ht2Entry ht2Entry)
                throws InvalidHt2EntryException {
            Matcher mv = VIDEO_TYPE_PATTERN.matcher(ht2Entry.getVideoType());
            if (!mv.matches()) {
                throw new InvalidHt2EntryException("Unknown video type syntax");
            }
            videoBitrate = Integer.parseInt(mv.group(1));
            videoMode = mv.group(2);

            Matcher ma = AUDIO_TYPE_PATTERN.matcher(ht2Entry.getAudioType());
            if (!ma.matches()) {
                throw new InvalidHt2EntryException("Unknown audio type syntax");
            }
            audioBitrate = Integer.parseInt(ma.group(1));
        }
        
        private void initializeEconomy(Ht2Entry ht2Entry, NicoIdInfoCache.Entry idInfo) {
            Boolean videoEconomy = idInfo.getDmcVideoEconomy(ht2Entry.getVideoType());
            Boolean audioEconomy = idInfo.getDmcAudioEconomy(ht2Entry.getAudioType());
            if (videoEconomy == null || audioEconomy == null) {
                economy = false;
                return;
            }
            economy = videoEconomy || audioEconomy;
        }

        private void initializeCache(NicoIdInfoCache.Entry idInfo) {
            video = VideoDescriptor.newDmc(smid, postfix, isLowAccess(),
                    videoMode, videoBitrate, audioBitrate, srcId);
            VideoDescriptor rvideo = Cache.getRegisteredVideoDescriptor(video);
            if (rvideo != null) {
                video = rvideo;
            }
            if (idInfo == null) {
                cache = new Cache(video);
            } else {
                cache = new Cache(video, idInfo.getTitle());
            }
            if (!isLowAccess()) {
                cache.unmarkLow();
            }
        }

        public Cache getCache() {
            return cache;
        }

        public NicoIdInfoCache.Entry getIdInfo() {
            return idInfo;
        }
        
        public String getSmid() {
            return smid;
        }

        public String getType() {
            return type;
        }

        public String getVideoId() {
            return videoId;
        }
        
        public String getPostfix() {
            return postfix;
        }
        
        public boolean isFlash() {
            return ht2Entry.isFlash();
        }

        public boolean isLowAccess() {
            return economy;
        }
        
        public VideoDescriptor getVideoDescriptor() {
            return video;
        }
    }

}
