package freenet.store.caching;

import freenet.keys.KeyVerifyException;
import freenet.node.SemiOrderedShutdownHook;
import freenet.store.*;
import freenet.support.ByteArrayWrapper;
import freenet.support.LRUMap;
import freenet.support.Logger;
import freenet.support.Ticker;
import freenet.support.io.NativeThread;

import java.io.IOException;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * CachingFreenetStore
 * 
 * @author Simon Vocella <voxsim@gmail.com>
 * 
 */
public class CachingFreenetStore<T extends StorableBlock> extends ProxyFreenetStore<T> {
    private static volatile boolean logMINOR;
 
	private boolean shuttingDown; /* If this flag is true, we don't accept puts anymore */
	private final LRUMap<ByteArrayWrapper, Block<T>> blocksByRoutingKey;
	private final StoreCallback<T> callback;
	private final boolean collisionPossible;
	private final ReadWriteLock configLock = new ReentrantReadWriteLock();
	private final CachingFreenetStoreTracker tracker;
	private final int sizeBlock;
	
    static { Logger.registerClass(CachingFreenetStore.class); }
    
	private final static class Block<T> {
		T block;
		byte[] data;
		byte[] header;
		boolean overwrite;
		boolean isOldBlock;
	}

	public CachingFreenetStore(StoreCallback<T> callback, FreenetStore<T> backDatastore, CachingFreenetStoreTracker tracker) {
		super(backDatastore);
		this.callback = callback;
		SemiOrderedShutdownHook shutdownHook = SemiOrderedShutdownHook.get();
		this.blocksByRoutingKey = LRUMap.createSafeMap(ByteArrayWrapper.FAST_COMPARATOR);
		this.collisionPossible = callback.collisionPossible();
		this.shuttingDown = false;
		this.tracker = tracker;
		this.sizeBlock = callback.getTotalBlockSize();
		
		callback.setStore(this);
		shutdownHook.addEarlyJob(new NativeThread("Close CachingFreenetStore", NativeThread.HIGH_PRIORITY, true) {
			@Override
			public void realRun() {
				innerClose(); // SaltedHashFS has its own shutdown job.
			}
		});
	}

	@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);
		
		Block<T> block = null;
		
		configLock.readLock().lock();
		try {
			block = blocksByRoutingKey.get(key);
		} finally {
			configLock.readLock().unlock();
		}
		
		if(block != null) {
			try {
				return this.callback.construct(block.data, block.header, routingKey, block.block.getFullKey(), canReadClientCache, canReadSlashdotCache, meta, null);
			} catch (KeyVerifyException e) {
				Logger.error(this, "Error in fetching for CachingFreenetStore: "+e, e);
			}
		}
		
		return backDatastore.fetch(routingKey, fullKey, dontPromote, canReadClientCache, canReadSlashdotCache, ignoreOldBlocks, meta);	
	}

	@Override
	public boolean probablyInStore(byte[] routingKey) {
		ByteArrayWrapper key = new ByteArrayWrapper(routingKey);
		Block<T> block = null;
		
		configLock.readLock().lock();
		try {
			block = blocksByRoutingKey.get(key);
		} finally {
			configLock.readLock().unlock();
		}
		
		return block != null || backDatastore.probablyInStore(routingKey);
	}
	
	@Override
	public void put(T block, byte[] data, byte[] header,
			boolean overwrite, boolean isOldBlock) throws IOException, KeyCollisionException {
		byte[] routingKey = block.getRoutingKey();
		final ByteArrayWrapper key = new ByteArrayWrapper(routingKey);
		
		Block<T> storeBlock = new Block<>();
		storeBlock.block = block;
		storeBlock.data = data;
		storeBlock.header = header;
		storeBlock.overwrite = overwrite;
		storeBlock.isOldBlock = isOldBlock;
		
		boolean cacheIt = true;
		
		//Case cache it
		configLock.writeLock().lock();
		
		try {
			if(!shuttingDown) {
				Block<T> previousBlock = blocksByRoutingKey.get(key);
			
				if(!collisionPossible || overwrite) {
					if(previousBlock == null) {
						cacheIt = tracker.add(sizeBlock);
					}
					
					if(cacheIt) {
						blocksByRoutingKey.push(key, storeBlock);
					}
				} else {
					//Case cache it but is it in blocksByRoutingKey? If so, throw a KCE
					if(previousBlock != null) {
						if(block.equals(previousBlock.block))
							return;
						throw new KeyCollisionException();
					}
					
					//Is probablyInStore()? If so, remove it from blocksByRoutingKey, and set a flag so we don't call put()
					if(backDatastore.probablyInStore(routingKey)) {
						cacheIt = false;
					} else {
						cacheIt = tracker.add(sizeBlock);
						
						if(cacheIt) {
							blocksByRoutingKey.push(key, storeBlock);
						}
					}
				}
			} else {
				cacheIt = false;
			}
		} finally {
			configLock.writeLock().unlock();
		}
		
		//Case don't cache it
		if(!cacheIt) {
			backDatastore.put(block, data, header, overwrite, isOldBlock);
			return;
		}
	}
	
	/** Try to write one block to disk.
	 * @return The number of bytes written to disk if we successfully wrote a block, 0 if we wrote 
	 * a block but can't remove it because it changed while we were writing it, and -1 if there 
	 * were no blocks to write because the cache is empty.
	 */
	long pushLeastRecentlyBlock() {
		Block<T> block = null;
		ByteArrayWrapper key = null;
		
		configLock.writeLock().lock();
		try {
			block = blocksByRoutingKey.peekValue();
			if(block == null) return -1;
			key = blocksByRoutingKey.peekKey();
		} finally {
			configLock.writeLock().unlock();
		}
			
		try {
			backDatastore.put(block.block, block.data, block.header, block.overwrite, block.isOldBlock);
		} catch (IOException e) {
			Logger.error(this, "Error in pushAll for CachingFreenetStore: "+e, e);
		} catch (KeyCollisionException e) {
			if(logMINOR) Logger.minor(this, "KeyCollisionException in pushAll for CachingFreenetStore: "+e, e);
		}
		
		configLock.writeLock().lock();
		try {
			Block<T> currentVersionOfBlock = blocksByRoutingKey.get(key);
			
			/** it might have changed if there was a put() with overwrite=true. 
			 *  If it has changed, return 0 , i.e. don't remove it*/
			if(currentVersionOfBlock != null && currentVersionOfBlock.block.equals(block.block)) {
				if(blocksByRoutingKey.removeKey(key))
					return sizeBlock;
			}
		} finally {
			configLock.writeLock().unlock();
		}
		return 0;
	}

	@Override
	public boolean start(Ticker ticker, boolean longStart) throws IOException {
		tracker.registerCachingFS(this);
		return this.backDatastore.start(ticker, longStart);
	}

	@Override
	public void close() {
		innerClose();
		backDatastore.close();
	}

	/** Close this store but not the underlying store. */
	private void innerClose() {
		configLock.writeLock().lock();
		try {
			shuttingDown = true;
			tracker.unregisterCachingFS(this);
		} finally {
			configLock.writeLock().unlock();
		}
	}
	
	/** Only for unit tests */
	boolean isEmpty() {
		boolean isEmpty;
		configLock.readLock().lock();
		try {
			isEmpty = this.blocksByRoutingKey.isEmpty();
		} finally {
			configLock.readLock().unlock();
		}
		return isEmpty;
	}
}
