package freenet.store.caching;

import java.util.ArrayList;

import freenet.support.Logger;
import freenet.support.Ticker;

/**
 * Tracks the memory used by a bunch of CachingFreenetStore's, and writes blocks to disk when full or 
 * after 5 minutes. One major objective here is we should not do disk I/O inside a lock, all methods 
 * should be non-blocking, even if it means the caller needs to do a blocking disk write.
 * 
 * @author Simon Vocella <voxsim@gmail.com>
 * 
*/
public class CachingFreenetStoreTracker {
    private static volatile boolean logMINOR;
    
    /** Number of keys that it's pushed to the *underlying* store in the add function.
     * FIXME make this configurable??? */
    private static int numberOfKeysToWrite = 20;
    
    /** Lower threshold, when it will start a write job, but still accept the data. */
    private static double lowerThreshold = 0.9;
    
    private final long maxSize;
	private final long period;
	private final ArrayList<CachingFreenetStore<?>> cachingStores;
	private final Ticker ticker;
	
	/** Is a write job queued for some point in the next period? There should only be one such job 
	 * queued. However if we then run out of memory we will run a job immediately. */
	private boolean queuedJob;
	/** Is a write job running right now? This prevents us from running multiple pushAllCachingStores() 
	 * in parallel and thus wasting memory, even if we run out of memory and so have to run a job
	 * straight away. */
	private boolean runningJob;
	private long size;
	
    static { Logger.registerClass(CachingFreenetStore.class); }
    
	public CachingFreenetStoreTracker(long maxSize, long period, Ticker ticker) {
		if(ticker == null)
			throw new IllegalArgumentException();
		this.size = 0;
		this.maxSize = maxSize;
		this.period = period;
		this.queuedJob = false;
		this.cachingStores = new ArrayList<CachingFreenetStore<?>>();
		this.ticker = ticker;
	}

	/** register a CachingFreenetStore to be called when we get full or to flush all after a setted period. */
	public void registerCachingFS(CachingFreenetStore<?> fs) {
		synchronized (cachingStores) {
			cachingStores.add(fs);
		}
	}
	
	public void unregisterCachingFS(CachingFreenetStore<?> fs) {
		long sizeBlock = 0;
		while(true) {
			sizeBlock = fs.pushLeastRecentlyBlock();
			synchronized(this) {
				if(sizeBlock == -1)
					break;
				else
					size -= sizeBlock;
			}
		}
		
		synchronized (cachingStores) {			
			cachingStores.remove(fs);
		}
	}
	
	/** If we are close to the limit, we will schedule an off-thread job to flush ALL the caches. 
	 *  Even if we are not, we schedule one after period. If we are at the limit, we will return 
	 *  false, and the caller should write directly to the underlying store.  */
	public synchronized boolean add(long sizeBlock) {
		/**  Here have a lower threshold, say 90% of maxSize, when it will start a write job, but 
		 * still accept the data. */
	    boolean justStartedPush = false;
		if(this.size + sizeBlock > this.maxSize*lowerThreshold) {
		    pushOffThreadNow();
		    justStartedPush = true;
		}
		//Check max size
		if(this.size + sizeBlock > this.maxSize) {
			// Over the limit, caller must write directly.
			// A delayed write is probably scheduled already. This is not a problem.
			// FIXME maybe we should remove it?
			return false;
		} else {
			this.size += sizeBlock;
			if(!justStartedPush) {
			    // Write everything to disk after the maximum delay (period), unless there is already
			    // a job scheduled to write to disk before that.
			    pushOffThreadDelayed();
			} // Else will be written anyway.
			return true;
		}
	}

    private synchronized void pushOffThreadNow() {
        if(runningJob) return;
        runningJob = true;
        this.ticker.queueTimedJob(new Runnable() {
            @Override
            public void run() {
                try {
                    pushAllCachingStores();
                } finally {
                    runningJob = false;
                }
            }
        }, 0);
    }

	private void pushOffThreadDelayed() {
	    if(queuedJob) return;
	    queuedJob = true;
	    this.ticker.queueTimedJob(new Runnable() {
	        @Override
	        public void run() {
	            synchronized(this) {
	                if(runningJob) return;
	                runningJob = true;
	            }
	            try {
	                pushAllCachingStores();
	            } finally {
	                synchronized(this) {
	                    queuedJob = false;
	                    runningJob = false;
	                }
	            }
	        }
	    }, period);
    }

	void pushAllCachingStores() {
		CachingFreenetStore<?>[] cachingStoresSnapshot = null;
		
		while(true) {
		    // Need to re-check occasionally in case new stores have been added.
	        synchronized (cachingStores) {
	            cachingStoresSnapshot = this.cachingStores.toArray(new CachingFreenetStore[cachingStores.size()]);
	        }
			for(CachingFreenetStore<?> cfs : cachingStoresSnapshot) {
				int k=0;
				while(k < numberOfKeysToWrite) {
					long sizeBlock = cfs.pushLeastRecentlyBlock();
					if(sizeBlock == -1) break;
					synchronized(this) {
						size -= sizeBlock;
						assert(size >= 0); // Break immediately if in unit testing.
						if(size < 0) {
							Logger.error(this, "Cache broken: Size = "+size);
							size = 0;
						}
						if(size == 0) return;
					}
					k++;
				}
			}
		}
	}
	
	public long getSizeOfCache() {
		long sizeReturned;
		synchronized(this) {
			sizeReturned = size;
		}
		return sizeReturned;
	}
}
