package freenet.store;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import freenet.keys.KeyVerifyException;
import freenet.node.stats.StoreAccessStats;
import freenet.node.useralerts.UserAlertManager;
import freenet.support.ByteArrayWrapper;
import freenet.support.LRUMap;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Ticker;
import freenet.support.Logger.LogLevel;
import freenet.support.api.Bucket;
import freenet.support.io.TempBucketFactory;

/** Short-term cache. Used to cache all blocks retrieved in the last 30 minutes (on low 
 * security levels), or just to cache data fetched through ULPRs (on higher security levels).
 * - Strict LRU.
 * - Size limit.
 * - Strictly enforced time limit.
 * - Blocks are encrypted, and kept in temp files.
 * 
 * @author Matthew Toseland <toad@amphibian.dyndns.org> (0xE43DA450)
 */
public class SlashdotStore<T extends StorableBlock> implements FreenetStore<T> {

	private static volatile boolean logDEBUG;

	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this);
			}
		});
	}
	
	private class DiskBlock {
		Bucket data;
		long lastAccessed;
	}
	
	private final TempBucketFactory bf;
	
	private long maxLifetime;
	
	private final long purgePeriod;
	
	// PURGING OLD DATA:
	// Every X period? I don't think it matters if we're a few minutes out, and it's probably easiest that way...
	
	private final Ticker ticker;
	
	private final LRUMap<ByteArrayWrapper, DiskBlock> blocksByRoutingKey;
	
	private final StoreCallback<T> callback;
	
	private int maxKeys;
	
	private long hits;
	private long misses;
	private long writes;
	
	private final int headerSize;
	private final int dataSize;
	private final int fullKeySize;
	
	public SlashdotStore(StoreCallback<T> callback, int maxKeys, long maxLifetime, long purgePeriod, Ticker ticker, TempBucketFactory tbf) {
		this.callback = callback;
		this.blocksByRoutingKey = LRUMap.createSafeMap(ByteArrayWrapper.FAST_COMPARATOR);
		this.maxKeys = maxKeys;
		this.bf = tbf;
		this.ticker = ticker;
		this.maxLifetime = maxLifetime;
		this.purgePeriod = purgePeriod;
		callback.setStore(this);
		this.headerSize = callback.headerLength();
		this.dataSize = callback.dataLength();
		this.fullKeySize = callback.fullKeyLength();
		Runnable purgeOldData = new Runnable() {

			@Override
			public void run() {
				try {
					purgeOldData();
				} finally {
					SlashdotStore.this.ticker.queueTimedJob(this, SlashdotStore.this.purgePeriod);
				}
			}

		};
		ticker.queueTimedJob(purgeOldData, maxLifetime + purgePeriod);
	}
	
	/**
	 * @param meta IGNORED!
	 */
	@Override
	public T fetch(byte[] routingKey, byte[] fullKey, boolean dontPromote, boolean canReadClientCache, boolean canReadSlashdotCache, boolean ignoreOldBlocks, BlockMetadata meta) throws IOException {
		ByteArrayWrapper key = new ByteArrayWrapper(routingKey);
		DiskBlock block;
		long timeAccessed;
		synchronized(this) {
			block = blocksByRoutingKey.get(key);
			if(block == null) {
				misses++;
				return null;
			}
			timeAccessed = block.lastAccessed;
		}
		byte[] fk = new byte[fullKeySize];
		byte[] header = new byte[headerSize];
		byte[] data = new byte[dataSize];
		InputStream in = block.data.getInputStream();
		try {
			DataInputStream dis = new DataInputStream(in);
			dis.readFully(fk);
			dis.readFully(header);
			dis.readFully(data);
		} finally {
			in.close();
		}
		try {
			T ret =
				callback.construct(data, header, routingKey, fk, canReadClientCache, canReadSlashdotCache, null, null);
			synchronized(this) {
				hits++;
				if(!dontPromote) {
					block.lastAccessed = System.currentTimeMillis();
					blocksByRoutingKey.push(key, block);
				}
			}
			if(logDEBUG) Logger.debug(this, "Block was last accessed "+(System.currentTimeMillis() - timeAccessed)+"ms ago");
			return ret;
		} catch (KeyVerifyException e) {
			block.data.free();
			synchronized(this) {
				blocksByRoutingKey.removeKey(key);
				misses++;
			}
			return null;
		}
	}

	@Override
	public long getBloomFalsePositive() {
		return -1;
	}

	@Override
	public long getMaxKeys() {
		return maxKeys;
	}

	@Override
	public long hits() {
		return hits;
	}

	@Override
	public long keyCount() {
		return blocksByRoutingKey.size();
	}

	@Override
	public long misses() {
		return misses;
	}

	@Override
	public boolean probablyInStore(byte[] routingKey) {
		ByteArrayWrapper key = new ByteArrayWrapper(routingKey);
		return blocksByRoutingKey.containsKey(key);
	}

	/**
	 * @param isOldBlock Ignored, we don't distinguish between stuff that should be cached and
	 * stuff that shouldn't be cached; really it's all in the latter category anyway here!
	 */
	@Override
	public void put(T block, byte[] data, byte[] header, boolean overwrite, boolean isOldBlock) throws IOException, KeyCollisionException {
		byte[] routingkey = block.getRoutingKey();
		byte[] fullKey = block.getFullKey();
		
		Bucket bucket = bf.makeBucket(fullKeySize + dataSize + headerSize);
		OutputStream os = bucket.getOutputStream();
		try {
		os.write(fullKey);
		os.write(header);
		os.write(data);
		} finally {
		os.close();
		}
		
		DiskBlock stored = new DiskBlock();
		stored.data = bucket;
		purgeOldData(new ByteArrayWrapper(routingkey), stored);
	}

	@Override
	public void setMaxKeys(long maxStoreKeys, boolean shrinkNow) throws IOException {
		if(maxStoreKeys > Integer.MAX_VALUE) throw new IllegalArgumentException();
		this.maxKeys = (int) maxStoreKeys;
		if(shrinkNow) {
			purgeOldData();
		} else {
			ticker.queueTimedJob(new Runnable() {

				@Override
				public void run() {
					purgeOldData();
					// Don't re-schedule
				}
				
			}, 0);
		}
	}

	@Override
	public long writes() {
		return writes;
	}

	protected void purgeOldData() {
		purgeOldData(null, null);
	}
	
	protected void purgeOldData(ByteArrayWrapper key, DiskBlock addFirst) {
		List<DiskBlock> blocks = null;
		DiskBlock oldBlock;
		synchronized(this) {
			long now = System.currentTimeMillis();
			if(addFirst != null) {
				addFirst.lastAccessed = now;
				oldBlock = blocksByRoutingKey.push(key, addFirst);
				if(oldBlock != null) {
	                if(blocks == null) blocks = new ArrayList<DiskBlock>();
	                blocks.add(oldBlock);
				}
				writes++;
			}
			while(true) {
				if(blocksByRoutingKey.isEmpty()) break;
				DiskBlock block = blocksByRoutingKey.peekValue();
				if(now - block.lastAccessed < maxLifetime && blocksByRoutingKey.size() < maxKeys) break;
				if(blocks == null) blocks = new ArrayList<DiskBlock>();
				blocks.add(block);
				blocksByRoutingKey.popValue();
			}
		}
		if(blocks == null) return;
		for(DiskBlock block : blocks) {
			block.data.free();
		}
	}

	public synchronized Long getLifetime() {
		return maxLifetime;
	}

	public synchronized void setLifetime(Long val) {
		maxLifetime = val;
	}
	
	@Override
	public StoreAccessStats getSessionAccessStats() {
		return new StoreAccessStats() {

			@Override
			public long hits() {
				return hits;
			}

			@Override
			public long misses() {
				return misses;
			}

			@Override
			public long falsePos() {
				return 0;
			}

			@Override
			public long writes() {
				return writes;
			}
			
		};
	}

	@Override
	public StoreAccessStats getTotalAccessStats() {
		return null;
	}

	@Override
	public boolean start(Ticker ticker, boolean longStart) throws IOException {
		return false;
	}

	@Override
	public void setUserAlertManager(UserAlertManager userAlertManager) {
		// Do nothing
	}

	@Override
	public FreenetStore<T> getUnderlyingStore() {
		return this;
	}

	@Override
	public void close() {
		// Do nothing
	}
}
