package dareka.processor.impl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dareka.common.CloseUtil;
import dareka.common.Config;
import dareka.common.ConfigObserver;
import dareka.common.FileUtil;
import dareka.common.Logger;
import dareka.processor.HttpHeader;
import dareka.processor.HttpRequestHeader;
import dareka.processor.HttpResponseHeader;
import dareka.processor.LocalFileResource;
import dareka.processor.Processor;
import dareka.processor.Resource;
import dareka.processor.StringResource;
import dareka.processor.URLResource;

// [nl] サムネイル画像をキャッシュしていくよ！(ファイル単位で保存版)
public class ThumbProcessor2 implements Processor, ConfigObserver {
    private static final String[] SUPPORTED_METHODS = new String[] { "GET" };
    private static final Pattern THUMB_URL_PATTERN = Pattern.compile(
            "^https?://(tn-skr)(\\d*)(\\.smilevideo\\.jp)(/smile\\?i=)(\\d{1,9})(?:\\.([ML]))?(?:|&\\.jpg)$");
    
    private static final ConcurrentHashMap<Integer, Object> retrievingLocks =
            new ConcurrentHashMap<Integer, Object>();
    
    private static final int THCACHE_SIGNATURE = 1203160629;
    private static final int THCACHE_VERSION = 2;
    private static final File thcacheFile  = new File("thcache.dat");
    private static final ConcurrentHashMap<Integer, File> thcacheSubFolders =
            new ConcurrentHashMap<Integer, File>();
    private static final Random rand = new Random();
    
    private static final String CONTENT_TYPE_JPG  = "image/jpeg";
    private static final String CONTENT_TYPE_GIF  = "image/gif";
    
    static class ThumbInfo {
        int id;
        String size;
        String contentType;
        byte[] contentBody;
        long lastModified;
        Resource resource;
        boolean status404;
        public ThumbInfo(int id, byte[] contentBody) {
            this.id = id;
            this.contentBody = contentBody;
            if (contentBody.length > 0) {
                if (contentBody[0] == (byte)0xff) {
                    this.contentType = CONTENT_TYPE_JPG;
                } else if (contentBody[0] == (byte)0x47) {
                    this.contentType = CONTENT_TYPE_GIF;
                }
            }
            this.lastModified = 0L; // Epoch
            this.resource = new StringResource(contentBody);
            setHeaderInfo();
        }
        public ThumbInfo(int id, String size, File thumbFile) {
            this.id = id;
            this.size = size;
            String path = thumbFile.getName().toLowerCase();
            if (path.endsWith(".jpg")) {
                this.contentType = CONTENT_TYPE_JPG;
            } else if (path.endsWith(".gif")) {
                this.contentType = CONTENT_TYPE_GIF;
            }
            this.lastModified = thumbFile.lastModified();
            try {
                this.resource = new LocalFileResource(thumbFile);
                setHeaderInfo();
            } catch (IOException e) {
                Logger.debugWithThread(e);
            }
        }
        public ThumbInfo(int id, String size, HttpRequestHeader requestHeader) {
            this.id = id;
            this.size = size;
            try {
                fetch(requestHeader);
            } catch (IOException e) {
                Logger.debugWithThread(e.toString());
            }
        }
        private void setHeaderInfo() {
            if (resource != null) {
                if (contentType != null) {
                    resource.setResponseHeader(HttpHeader.CONTENT_TYPE, contentType);
                }
                if (lastModified >= 0L) {
                    resource.setLastModified(lastModified);
                }
                if (thcacheExpires > 0) {
                    resource.addCacheControlResponseHeaders(thcacheExpires);
                }
            }
        }
        private void fetch(HttpRequestHeader requestHeader) throws IOException {
            URLResource r = new URLResource(requestHeader.getURI());
            requestHeader.removeMessageHeader(HttpHeader.IF_MODIFIED_SINCE);
            requestHeader.removeMessageHeader("If-None-Match");
            r.setFollowRedirects(true);
            if (thcacheTimeout > 0) {
                // 10%のゆらぎを設けて同時タイムアウトを避ける
                int n = thcacheTimeout / 10;
                r.setTransferTimeout(thcacheTimeout + rand.nextInt(n * 2) - n);
            }
            ByteArrayOutputStream bout = new ByteArrayOutputStream(32 * 1024);
            r.transferTo(null, bout, requestHeader, null);
            ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
            HttpResponseHeader rh = new HttpResponseHeader(bin);
            
            int statusCode = rh.getStatusCode();
            if (statusCode == 200) {
                String s = rh.getMessageHeader(HttpHeader.CONTENT_TYPE);
                if (CONTENT_TYPE_JPG.equalsIgnoreCase(s)) {
                    contentType = CONTENT_TYPE_JPG;
                } else if (CONTENT_TYPE_GIF.equalsIgnoreCase(s)) {
                    contentType = CONTENT_TYPE_GIF;
                }
                lastModified = rh.getLastModified();
                contentBody = new byte[bin.available()];
                bin.read(contentBody);
                resource = StringResource.getRawResource(rh, contentBody);
            }
            status404 = statusCode == 404;
            if (Boolean.getBoolean("swfDebug")) {
                Logger.info("thcache: %s (status=%d,len=%d)",
                        requestHeader.getURI(), statusCode, rh.getContentLength());
            }
        }
        public byte[] getBody() {
            if (resource != null && resource instanceof URLResource) {
                URLResource r = (URLResource)resource;
                try {
                    contentBody = r.getResponseBody();
                } catch (IOException e) {
                    Logger.debugWithThread(e);
                }
            }
            return contentBody;
        }
        public String getExt() {
            if (contentType == CONTENT_TYPE_JPG) {
                return ".jpg";
            } else if (contentType == CONTENT_TYPE_GIF) {
                return ".gif";
            }
            return "";
        }
    }
    
    private static File thcacheFolder;
    private static int thcacheTimeout, thcacheExpires;
    
    public static void init() {
        if (thcacheFolder == null) {
            Config.addObserver(new ThumbProcessor2());
        }
    }
    
    public void update(Config config) {
        thcacheFolder = new File(System.getProperty("thcacheFolder"));
        thcacheFolder.mkdir();
        thcacheSubFolders.clear();
        thcacheTimeout = Integer.getInteger("thcacheTimeout", 0);
        thcacheExpires = Integer.getInteger("thcacheExpires", 0) * 3600 * 24;
    }
    
    public ThumbProcessor2() {
        if (!thcacheFile.exists()) return;
        
        Logger.info("Converting '" + thcacheFile.getName() + "' to individual files:");
        RandomAccessFile raf = null;
        try {
            raf = new RandomAccessFile(thcacheFile, "r");
            if (raf.readInt() == THCACHE_SIGNATURE && raf.readInt() == THCACHE_VERSION) {
                int count = raf.readInt(), converted = 0;
                raf.readLong(); // savedRequest
                raf.readLong(); // savedTransfer
                for (int i = 0; i < count; i++) {
                    int id  = raf.readInt();
                    int len = raf.readInt();
                    byte[] content = new byte[len];
                    raf.readFully(content);
                    ThumbInfo ti = new ThumbInfo(id, content);
                    if (storeCache(ti, false)) converted++;
                    if (i % 1000 == 0) System.out.print(".");
                }
                System.out.println();
                Logger.info(String.format(" => %,d files converted, %,d files skipped.",
                        converted, count - converted));
            } else {
                Logger.info(" => invalid cache format, not converted.");
            }
        } catch (IOException e) {
            Logger.error(e);
        } finally {
            CloseUtil.close(raf);
        }
        thcacheFile.renameTo(new File(thcacheFile.getPath() + ".bak"));
    }
    
    private File getCacheFile(int id, String size, String ext) {
        Integer key = id / 100000;
        File subFolder = thcacheSubFolders.get(key);
        if (subFolder == null) {
            assert thcacheFolder != null;
            subFolder = new File(thcacheFolder, String.format("%03d", key));
            subFolder.mkdir();
            thcacheSubFolders.put(key, subFolder);
        }
        if (size == null) {
            return new File(subFolder, String.format("%05d%s", id % 100000, ext));
        } else {
            return new File(subFolder, String.format("%05d.%s%s", id % 100000, size, ext));
        }
    }
    
    private boolean storeCache(ThumbInfo ti, boolean overwrite) {
        File cacheFile = getCacheFile(ti.id, ti.size, ti.getExt());
        if (!cacheFile.exists() || overwrite) {
            byte[] data = ti.getBody();
            if (data != null && data.length > 0) {
                int len = FileUtil.copy(ByteBuffer.wrap(data), cacheFile);
                if (len > 0 && ti.lastModified >= 0L) {
                    cacheFile.setLastModified(ti.lastModified);
                    Logger.debugWithThread(cacheFile.getPath() + " stored");
                    return true;
                }
            }
        }
        return false;
    }
    
    // Processorの実装
    public String[] getSupportedMethods() {
        return SUPPORTED_METHODS;
    }
    
    public Pattern getSupportedURLAsPattern() {
        return THUMB_URL_PATTERN;
    }
    
    public String getSupportedURLAsString() {
        return null;
    }
    
    public Resource onRequest(HttpRequestHeader requestHeader, Socket browser)
            throws IOException {
        String uri = requestHeader.getURI();
        Matcher m = THUMB_URL_PATTERN.matcher(uri);
        if (!m.find()) {
            Logger.warning("Unexpected thumbnail request: " + uri);
            return new URLResource(uri);
        }
        int id = Integer.parseInt(m.group(5));
        String size = m.group(6);
        Resource r = getThumbResourceFromCache(id, size, requestHeader,
                Boolean.getBoolean("thcacheFixEpoch"));
        if (!Boolean.getBoolean("thcacheLargeSize") && size != null) {
            // thcacheLargeSizeがfalseの時は.Mや.Lを新たにキャッシュしない
            return new URLResource(uri);
        }
        if (r == null) {
            // キャッシュに無ければサムネ鯖から取得
            // 多重取得しないように既に取得中ならキャッシュするまで待つ
            Object lock = retrievingLocks.putIfAbsent(id, m); // mをロックオブジェクトに転用
            if (lock != null) {
                synchronized (lock) {
                    r = getThumbResourceFromCache(id, size, requestHeader, false);
                }
            } else {
                lock = m;
                synchronized (lock) {
                    r = getThumbResourceFromServer(id, size, requestHeader, m);
                    retrievingLocks.remove(id);
                }
            }
        }
        return r != null ? r : StringResource.getNotFound();
    }
    
    private Resource getThumbResourceFromCache(int id, String size,
            HttpRequestHeader requestHeader, boolean fixEpoch) {
        File cacheFile = getCacheFile(id, size, ".jpg");
        if (!cacheFile.exists()) {
            cacheFile = getCacheFile(id, size, ".gif");
            if (!cacheFile.exists()) return null;
        }
        if (fixEpoch && cacheFile.lastModified() == 0L) {
            // サムネ鯖から取得できない場合に備えてEpochを現在時刻にしておく
            cacheFile.setLastModified(System.currentTimeMillis());
            return null;
        }
        if (cacheFile.length() == 0L) {
            return getReplace404(0, null);
        }
        return new ThumbInfo(id, size, cacheFile).resource;
    }
    
    private Resource getThumbResourceFromServer(int id, String size,
            HttpRequestHeader requestHeader, Matcher m) {
        ThumbInfo ti = new ThumbInfo(id, size, requestHeader);
        boolean status404 = ti.status404;
        if (ti.resource == null) {
            // 指定のサムネ鯖から取得できない場合はラウンドロビンで回す
            final String path = m.group(4) + m.group(5) + (m.group(6) != null ? "." + m.group(6) : "");
            // 数字無し鯖の場合はtn-skr1と仮定(実際、DNS上もそうなってるみたいだし)
            final int start = m.group(2).equals("") ? 1 : Integer.parseInt(m.group(2));
            int num = start;
            while (++num != start && ti.resource == null) {
                String host = m.group(1) + num + m.group(3);
                try {
                    // 次のサムネ鯖があるか？
                    InetAddress.getByName(host);
                } catch (UnknownHostException e) {
                    num = 0; continue;
                }
                requestHeader.setURI(requestHeader.getScheme() + "://" + host + path);
                requestHeader.setMessageHeader("Host", host);
                ti = new ThumbInfo(id, size, requestHeader);
                status404 &= ti.status404;
            }
        }
        if (ti.resource == null) {
            Logger.info("thcache: " + m.group() + " failed");
            // 全てのサムネ鯖が404を返す場合は次回アクセスしないように代替サムネを使う
            if (status404) {
                // fixEpochの場合はキャッシュにあるので取得してみる
                Resource r = getThumbResourceFromCache(id, size, null, false);
                if (r == null) {
                    r = getReplace404(id, size);
                }
                return r;
            }
        } else {
            storeCache(ti, true);
        }
        return ti.resource;
    }
    
    private Resource getReplace404(int id, String size) {
        String replace404 = System.getProperty("thcacheReplace404", "");
        if (replace404.matches("https?://.*")) {
            if (id > 0) {
                createZeroFile(id, size);
            }
            return StringResource.getRedirect(replace404);
        }
        File thumb404 = new File(replace404);
        ThumbInfo ti = new ThumbInfo(id, size, thumb404);
        if (id > 0 && thumb404.length() > 0L) {
            String mode = System.getProperty("thcacheReplace404Mode");
            if ("copy".equals(mode)) {
                File f = getCacheFile(id, size, ti.getExt());
                long len = FileUtil.copy(thumb404, f);
                if (len > 0L && ti.lastModified >= 0L) {
                    f.setLastModified(ti.lastModified);
                }
            } else {
                createZeroFile(id, size);
            }
        }
        return ti.resource;
    }
    
    private void createZeroFile(int id, String size) {
        File f = getCacheFile(id, size, ".jpg");
        if (!f.exists()) {
            try {
                new FileOutputStream(f).close();
            } catch (IOException e) {
                Logger.error(e);
            }
        }
    }
    
}
