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(
			"^http://(tn-skr)(\\d*)(\\.smilevideo\\.jp)(/smile\\?i=)(\\d{1,9})(?:|&\\.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 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, File thumbFile) {
			this.id = id;
			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, HttpRequestHeader requestHeader) {
			this.id = id;
			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 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);
		}
		return new File(subFolder, String.format("%05d%s", id % 100000, ext));
	}
	
	private boolean storeCache(ThumbInfo ti, boolean overwrite) {
		File cacheFile = getCacheFile(ti.id, 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));
		Resource r = getThumbResourceFromCache(id, requestHeader,
				Boolean.getBoolean("thcacheFixEpoch"));
		if (r == null) {
			// キャッシュに無ければサムネ鯖から取得
			// 多重取得しないように既に取得中ならキャッシュするまで待つ
			Object lock = retrievingLocks.get(id);
			if (lock != null) {
				synchronized (lock) {
					r = getThumbResourceFromCache(id, requestHeader, false);
				}
			} else {
				lock = m; // ロックオブジェクトに転用
				synchronized (lock) {
					retrievingLocks.put(id, lock);
					r = getThumbResourceFromServer(id, requestHeader, m);
					retrievingLocks.remove(id);
				}
			}
		}
		return r != null ? r : StringResource.getNotFound();
	}
	
	private Resource getThumbResourceFromCache(int id,
			HttpRequestHeader requestHeader, boolean fixEpoch) {
		File cacheFile = getCacheFile(id, ".jpg");
		if (!cacheFile.exists()) {
			cacheFile = getCacheFile(id, ".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);
		}
		return new ThumbInfo(id, cacheFile).resource;
	}
	
	private Resource getThumbResourceFromServer(int id,
			HttpRequestHeader requestHeader, Matcher m) {
		ThumbInfo ti = new ThumbInfo(id, requestHeader);
		boolean status404 = ti.status404;
		if (ti.resource == null) {
			// 指定のサムネ鯖から取得できない場合はラウンドロビンで回す
			final String path = m.group(4) + m.group(5);
			// 数字無し鯖の場合は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("http://" + host + path);
				requestHeader.setMessageHeader("Host", host);
				ti = new ThumbInfo(id, requestHeader);
				status404 &= ti.status404;
			}
		}
		if (ti.resource == null) {
			Logger.info("thcache: " + m.group() + " failed");
			// 全てのサムネ鯖が404を返す場合は次回アクセスしないように代替サムネを使う
			if (status404) {
				// fixEpochの場合はキャッシュにあるので取得してみる
				Resource r = getThumbResourceFromCache(id, null, false);
				if (r == null) {
					r = getReplace404(id);
				}
				return r;
			}
		} else {
			storeCache(ti, true);
		}
		return ti.resource;
	}
	
	private Resource getReplace404(int id) {
		String replace404 = System.getProperty("thcacheReplace404", "");
		if (replace404.startsWith("http://")) {
			createZeroFile(id);
			return StringResource.getRedirect(replace404);
		}
		File thumb404 = new File(replace404);
		ThumbInfo ti = new ThumbInfo(id, thumb404);
		if (id > 0 && thumb404.length() > 0L) {
			String mode = System.getProperty("thcacheReplace404Mode");
			if ("copy".equals(mode)) {
				File f = getCacheFile(id, ti.getExt());
				long len = FileUtil.copy(thumb404, f);
				if (len > 0L && ti.lastModified >= 0L) {
					f.setLastModified(ti.lastModified);
				}
			} else {
				createZeroFile(id);
			}
		}
		return ti.resource;
	}
	
	private void createZeroFile(int id) {
		File f = getCacheFile(id, ".jpg");
		if (!f.exists()) {
			try {
				new FileOutputStream(f).close();
			} catch (IOException e) {
				Logger.error(e);
			}
		}
	}
	
}
