package dareka.processor.impl;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dareka.common.CloseUtil;
import dareka.common.Logger;
import dareka.processor.HttpRequestHeader;
import dareka.processor.HttpResponseHeader;
import dareka.processor.Processor;
import dareka.processor.Resource;
import dareka.processor.StringResource;
import dareka.processor.TransferListener;
import dareka.processor.URLResource;

// [nl] サムネイル画像をキャッシュしていくよ！
public class ThumbProcessor implements Processor, TransferListener {
    private static final String[] SUPPORTED_METHODS = new String[] { "GET" };
    private static final Pattern SM_THUMB_PATTERN = Pattern
    		.compile("^http://tn-skr[^\\.]*\\.smilevideo\\.jp/smile\\?i=(\\d+)([^\\d].*)?$");
    
    private static int SIGNITURE = 1203160629;
    private static int VERSION = 2;
    
// kapaer
    private static ConcurrentHashMap<Integer, long[]> id2thum = 
    						new ConcurrentHashMap<Integer, long[]>();
    private static ConcurrentHashMap<Integer, ByteBuffer> id2thum_tmp = 
    						new ConcurrentHashMap<Integer, ByteBuffer>();
    private static AtomicLong savedRequest = new AtomicLong(); 
    private static AtomicLong savedTransfer = new AtomicLong();
    
    private static File thcache  = new File("thcache.dat");
    private static File thcacheIndex  = new File("thIndex.dat");

    private static boolean indexStoreMode = true;
    
    // サムネイルキャッシュの読み込み
    public static void init()
    {
		synchronized (ThumbProcessor.class) {
			if (!Boolean.getBoolean("quickThumbnailCache")) {
				thcacheIndex.delete();
				indexStoreMode = false;
			}

			if (indexStoreMode && thcacheIndex.exists() && thcache.exists()) {
				if (!init_fast()) {
					thcacheIndex.delete();
		    		init_v2();
				}
			} else if (thcache.exists()) {
				thcacheIndex.delete();
				init_v2();
			} else {
				thcacheIndex.delete();
			}
		}
    }
    
// インデックスを読み込む
    private static boolean init_fast()
    {
    	RandomAccessFile raf = null;
		long lastPos = 0;

// まずキャッシュのヘッダを読み込む
    	try {
			raf = new RandomAccessFile(thcache, "rw");
			int signiture = raf.readInt();
			int version = raf.readInt();
			if (signiture != SIGNITURE || version != VERSION)
			{
// ヘッダがおかしい
				return false;
			}
			@SuppressWarnings("unused") // 使わないので読み飛ばすだけ
			int size = raf.readInt();
			savedRequest.set(raf.readLong());
			savedTransfer.set(raf.readLong());
		} catch (FileNotFoundException e) {
		} catch (IOException e) {
			return false;
		}
		finally {
			CloseUtil.close(raf);
		}

// インデックス本体を読み込む
    	try {
			raf = new RandomAccessFile(thcacheIndex, "rw");
			int signiture = raf.readInt();
			int version = raf.readInt();
			if (signiture != SIGNITURE || version != VERSION)
			{
// ヘッダがおかしかったら作り直す
				return false;
			}

			while (raf.getFilePointer() < raf.length())
			{
				int key = raf.readInt();
				long point = raf.readLong();
				int len = raf.readInt();
				long[] bufPoint = {point, len};

				id2thum.put(key, bufPoint);
				lastPos = raf.getFilePointer();
			}
// ファイルサイズを設定する
// ひょっとしたらごみがついてるかもしれないので
			raf.setLength(lastPos);

		} catch (FileNotFoundException e) {
		} catch (IOException e) {
// エラーがあったら作り直す
			return false;
		}
		finally {
			CloseUtil.close(raf);
		}

		Logger.info("Thumbnail cache v2(quickStart): %,d files (saved %,d requests, %,d bytes)",
				id2thum.size(), savedRequest.get(), savedTransfer.get());

		return true;
    }

// インデックスをすべて保存する
    protected static void storeAllIndex()
    {
		boolean writeFailed = false;
    	RandomAccessFile raf = null;
    	try {
    		raf = new RandomAccessFile(thcacheIndex, "rw");
			raf.setLength(0);
    		raf.writeInt(SIGNITURE);
    		raf.writeInt(VERSION);

			for (Integer key: id2thum.keySet())
			{
				raf.writeInt(key);
				raf.writeLong(id2thum.get(key)[0]);
				raf.writeInt((int)id2thum.get(key)[1]);
			}
		} catch (FileNotFoundException e) {
			indexStoreMode = false;
			writeFailed = true;
			Logger.warning("Store "+thcacheIndex.getName()+" failed! QuickThumbnailCache OFF...");
		} catch (IOException e) {
			writeFailed = true;
			indexStoreMode = false;
			Logger.warning("Store "+thcacheIndex.getName()+" failed! QuickThumbnailCache OFF...");
		}
		finally
		{
			CloseUtil.close(raf);
		}
		if (writeFailed) {
			thcacheIndex.delete();
		}

    }


    // format version 2
    private static void init_v2()
    {
		id2thum.clear();
		id2thum_tmp.clear();
    	RandomAccessFile raf = null;
		long lastPos = 0;
		long lastLen = 0;
		int lastKey = 0;
		
    	try {
			raf = new RandomAccessFile(thcache, "rw");
			int signiture = raf.readInt();
			int version = raf.readInt();
			if (signiture != SIGNITURE || version != VERSION)
			{
// ヘッダがおかしかったらいきなり削除
				raf.setLength(0);
				return ;
			}

			@SuppressWarnings("unused") // 使わないので読み飛ばすだけ
			int size = raf.readInt();
			savedRequest.set(raf.readLong());
			savedTransfer.set(raf.readLong());
			while (raf.getFilePointer() < raf.length())
			{
				int key = raf.readInt();
				int len = raf.readInt();
				long[] bufPoint = {raf.getFilePointer(), len};
// 長さが妙だったら一つ前の奴の長さが狂ってると思われるので、
// それも削除する
				if (!(0 < len && len < 100000))
					throw new IOException();

// ヘッダが妙な奴は保存失敗してるとみなす
// ヘッダからの判定は、JPGとGIFがある(0xff か 0x47('G'))
				int head = raf.read();
				if (head != 0xff && head != 0x47) {
					throw new IOException();
				}
// 一つ前の奴までは正しいっぽいので、last**を更新
// エラーがあった時は、last**の示すキャッシュも削除される
// 美しくない。適当なクラスでもでっち上げるか。
				lastPos = bufPoint[0];
				lastLen = bufPoint[1];
				lastKey = key;

// EOFを超えてなければseek
				if (lastPos + lastLen > raf.length())
					throw new IOException();
				raf.seek(lastPos + lastLen);

				id2thum.put(key, bufPoint);
			}
// ファイルサイズを設定する
// ひょっとしたらごみがついてるかもしれないので
			if (lastKey != 0) {
				raf.setLength(lastPos+lastLen);
			}
		} catch (FileNotFoundException e) {
		} catch (IOException e) {
// 読み込みエラーなら、一つ前のキャッシュも削除
// readIntは32ビットなので、x2で8バイト
			id2thum.remove(lastKey);
			try {
				if (lastPos > 8)
					lastPos -= 8;
				raf.setLength(lastPos);
			} catch (IOException e1) {}
		}
		finally {
			CloseUtil.close(raf);
		}
		if (indexStoreMode) {
			storeAllIndex();
		}
		Logger.info("Thumbnail cache v2: %,d files (saved %,d requests, %,d bytes)",
				id2thum.size(), savedRequest.get(), savedTransfer.get());
    }
    
    // v2
    protected static void store(boolean silent)
    {
    	Set<Map.Entry<Integer, long[]>> kvSet = id2thum.entrySet();
    	RandomAccessFile raf = null;
    	RandomAccessFile rafIndex = null;
    	boolean writefailed = false;
    	try {
    		raf = new RandomAccessFile(thcache, "rw");
    		raf.writeInt(SIGNITURE);
    		raf.writeInt(VERSION);
// Index保存、サイズが小さいから同時に書き込んでも断片化はしない･･･と思う
			if (indexStoreMode) {
    			rafIndex = new RandomAccessFile(thcacheIndex, "rw");
    			rafIndex.writeInt(SIGNITURE);
    			rafIndex.writeInt(VERSION);
				rafIndex.seek(rafIndex.length());
			}
// サイズは保存時にはじかれて減るかもしれないけど、今は使ってないから放置
			raf.writeInt(kvSet.size()+id2thum_tmp.size());

			raf.writeLong(savedRequest.get());
			raf.writeLong(savedTransfer.get());
// テンポラリを後ろに追加する
			int size = id2thum_tmp.size();
			raf.seek(raf.length());
			for (Integer key: id2thum_tmp.keySet())
			{
				byte[] data = id2thum_tmp.get(key).array();
// ヘッダが妙な奴は保存しない
// ヘッダからの判定は、JPGとGIFがある(0xff か 0x47('G'))
				if (data[0] == (byte)0xff || data[0] == (byte)0x47) {
					raf.writeInt(key);
					raf.writeInt(data.length);
					long[] bufPoint = {raf.getFilePointer(), data.length};
					raf.write(data, 0, data.length);
					id2thum.put(key, bufPoint);
// Index保存
					if (indexStoreMode) {
						rafIndex.writeInt(key);
						rafIndex.writeLong(id2thum.get(key)[0]);
						rafIndex.writeInt((int)id2thum.get(key)[1]);
					}
				}
				id2thum_tmp.remove(key);
			}
			if (!silent)
				Logger.info("%d thumbs added", size);
		} catch (FileNotFoundException e) {
			Logger.warning("Unable to store " + thcache.getName());
		} catch (IOException e) {
			writefailed = true;
			Logger.warning("Storing thumb cache is failed!");
		}
		finally
		{
			CloseUtil.close(raf);
			CloseUtil.close(rafIndex);
			if (writefailed)
			{
// 書き込み失敗したら読み直し
// それで壊れてる分を切り捨てる
// ヘッダまで壊れてたら、initで削除される
				init_v2();
			}
		}
    }

    // Processorの実装
    Integer id;
	public String[] getSupportedMethods() {
		return SUPPORTED_METHODS;
	}

	public Pattern getSupportedURLAsPattern() {
		return SM_THUMB_PATTERN;
	}

	public String getSupportedURLAsString() {
		return null;
	}

	public Resource onRequest(HttpRequestHeader requestHeader, Socket browser)
			throws IOException {
		Matcher m = SM_THUMB_PATTERN.matcher(requestHeader.getURI());
		if (!m.find())
			return new URLResource(requestHeader.getURI());
		id = Integer.parseInt(m.group(1));
		
		ByteBuffer buf = id2thum_tmp.get(id);
		long[] bufPoint = id2thum.get(id);

		if (buf != null || bufPoint != null)
		{
			StringResource r = null;
			if (requestHeader.getMessageHeader("If-Modified-Since") != null) {
				savedRequest.incrementAndGet();
				r = StringResource.getNotModified();
			} else {
				byte[] data;
				if (buf != null) {
					data = buf.array();
				} else {
// ファイルから読み込む
			    	RandomAccessFile raf = null;
			    	try {
    					raf = new RandomAccessFile(thcache, "r");
						raf.seek(bufPoint[0]);
						data = new byte[(int)bufPoint[1]];
						raf.readFully(data);
					} catch (IOException e) {
						data = null;
					} finally {
						CloseUtil.close(raf);
					}
				}

// そのIDのサムネが読み込みエラーならキャッシュせずに返す
				if (data == null) {
					return new URLResource(requestHeader.getURI());
				}
				savedTransfer.addAndGet(data.length);
				long sR = savedRequest.incrementAndGet();

				synchronized (ThumbProcessor.class) {
					if (sR % 20 == 0)
						store(true);
				}
				r = new StringResource(data);
			}
			r.setResponseHeader("Content-Type", "image/jpeg");
			return r;
		}
		URLResource r = new URLResource(requestHeader.getURI());
		r.addTransferListener(this);
		return r;
	}

	// TransferListenerの実装
	private ByteArrayOutputStream out = null;
	public void onResponseHeader(HttpResponseHeader responseHeader) {
		if (responseHeader.getStatusCode() == 200)
			out = new ByteArrayOutputStream();
		//else
			//Logger.info(responseHeader.getReason() + " " + responseHeader.getStatusCode());
	}

	public void onTransferBegin(OutputStream receiverOut) { }

	public void onTransferring(byte[] buf, int length) {
		if (out != null)
			out.write(buf, 0, length);
	}

	public void onTransferEnd(boolean completed) {
		synchronized (ThumbProcessor.class)
		{
			if (completed && out != null && out.size() > 0)
			{
// とりあえずメモリ上のテンポラリに保存
// 溜まったらファイルに書き出して、テンポラリ削除
// 以降はファイルから読み込む
				id2thum_tmp.put(id, ByteBuffer.wrap(out.toByteArray()));
				if (id2thum_tmp.size() >= 30)
					store(false);
				out = null;
			}
		}
	}
}
