// NCache.java

import java.io.*;
import java.util.*;
import java.util.zip.*;

/** The NCache is like a Hashtable in lookup/insert/delete
 *  The NCache is persistent over sessions (saves itself to disk).
//  The NCache is selfcleaning, that is it removes old stuff.
 */
public class NCache extends Thread 
{
    private static String DIR = "cache";              // standard dir.
    private static long SIZE  = 10 * 1024 * 1024;     // 10 MB.

    private static String CACHEINDEX = "cache.index"; // the indexfile.
    private static String TEMPDIR = "temp";
    private static int filesperdir = 256;             // reasonable?

    private boolean changed = false;                  // have we changed?
    private long maxSize = 0;
    private long cacheTime = 0;
    private long fileNo = 0;
    private long dirNo  = 0;
    private long currentSize = 0;
    private String dir = null;
    private Hashtable htab = null;                    // mapping Object => CacheEntry.
    private Vector vec = null;

    private Hashtable keyHash = new Hashtable();      // mapping url to dirNo
    private File tempdir = null;

    /** Create a cache that uses some dir.
     */
    public NCache () {
	this (DIR);
    }

    /** Create a cache for the given directory.
     * @param dir the name of the directory where the cache should store its files.
     */
    public NCache (String dir) {
	this (dir, SIZE);
    }
    
    /** Create a cache for the given directory with given parameters.
     * @param dir the name of the directory where the cache should store its files.
     * @param maxSize the maximum size of the proxy.
     */
    public NCache (String dir, long maxSize) {
	htab = new Hashtable ();
	vec = new Vector ();
	setMaxSize (maxSize);
	setCacheDir (dir);
    } 

    /** Get the name of the directory where the cache stores its files.
     * @return the name of the cachedir
     */
    public String getCacheDir () {
	return dir;
    }
    
    /** Sets the cachedir. This will flush the cache and make 
     *  it try to read in the cache from the new dir.
     * @param newDir the name of the new directory to use.
     */
    public synchronized void setCacheDir (String newDir) {
	// save old cachedir.
	if (dir != null)
	    writeCacheIndex ();  
	
	// does new dir exist?
	dir = newDir;
	File dirtest = new File (dir);
	if (!dirtest.exists ()) {
	    dirtest.mkdirs ();
	    if (!dirtest.exists ()) {
		System.err.println ("couldnt create cachedir, exiting");
		System.exit (-1);
	    }
	}
	else if (dirtest.isFile ()) {
	    System.err.println ("Cachedir is a file, that is bad, exiting");
	    System.exit (-1);
	}
	
	tempdir = new File (dir + File.separator + TEMPDIR);
	if (!tempdir.exists ()) {
	    tempdir.mkdir ();
	    if (!tempdir.exists ()) {
		System.err.println ("couldlnt create cache tempdir, exiting");
		System.exit (-1);
	    }
	} 
	else if (tempdir.isFile ()) {
	    System.err.println ("Cache temp dir is a file, that is bad, exiting");
	    System.exit (-1);
	}

	// move to new dir.
	readCacheIndex ();
    }

    /** Get the maximum size for this cache.
     * @return the maximum size in bytes this cache.
     */
    public long getMaxSize () {
	return maxSize;
    }
    
    /** Set the maximum size for this cache.
     * @param newMaxSize the new maximum size for the cache.
     */
    public void setMaxSize (long newMaxSize) {
	maxSize = newMaxSize;
    }

    /** Get the current size of the cache
     * @return the current size of the cache in bytes.
     */
    public long getCurrentSize () {
	return currentSize;
    }

    /** Get the CacheEntry assosiated with given object.
     * @param o the key.
     * @return the NCacheEntry or null (if not found).
     */ 
    public NCacheEntry getEntry (Object o) {
	NCacheEntry ent =  (NCacheEntry)htab.get (o);
	/*if (ent != null)
	  ent.setVisited (new Date ());*/
	return ent;
    }

    /** Reserve space for a CacheEntry with key o.
     * @param o the key for the NCacheEntry.
     * @return a new CacheEntry initialized for the cache.
     */
     public synchronized NCacheEntry newEntry (Object o) {
	 String url = ((HTTPHeader)o).getRequestURI();
	 long entDir= -1;
	 if ((Long) keyHash.get(url) == null) {
	     keyHash.put(url, new Long(dirNo));
	     dirNo ++;
	 }
	 entDir = ((Long) keyHash.get(url)).longValue();

	 File newdir = new File (dir, ""+ entDir);
	 if (!newdir.exists ()) {
	     newdir.mkdir ();
	 } 
	 NCacheEntry entry = new NCacheEntry (o,
					      dir + File.separator + 
					      entDir + File.separator +
					      fileNo,
					      fileNo);
	 fileNo++;
	 return entry;
     }
    
    /** Insert a CacheEntry into the cache.
     * @param ent the CacheEntry to store.
     */
    public void addEntry (NCacheEntry ent) {
	if (ent == null)
	    return;	
	HTTPHeader rURI = (HTTPHeader) ent.getKey();
	String url = rURI.getRequestURI();
	long fdir = ((Long) keyHash.get(url)).longValue();
	String newName = dir + File.separator + fdir + File.separator + ent.getId ();
	File cfile = new File (ent.getFileName ());
	if (!cfile.exists()) {
	    return;
	}
	boolean bool = cfile.renameTo (new File (newName));
	if (bool) {
	    ent.setFileName (newName);	    
	}
	cfile = new File (ent.getFileName ());	
	ent.setSize (cfile.length ());

	synchronized (this) {
	    htab.put (ent.getKey (), ent);
	    currentSize += ent.getSize ();
	    vec.addElement (ent);
	}
	changed = true;
    }
    
    /** Remove the Entry with key o from the cache.
     * @param o the key for the CacheEntry.
     */
    public void remove (Object o) {
	synchronized (this) {
	    NCacheEntry rent = (NCacheEntry)htab.get (o);
	    if (rent != null) {
		File cfile = new File (rent.getFileName ());
		if (cfile.exists ()) {
		    String parent = cfile.getParent ();
		    cfile.delete ();
		    File p = new File (parent);
		    // humm, until NT does rename in a nice manner check for tempdir.
		    if (p.exists () && !p.equals (tempdir)) {		    
			String ls[] = p.list ();
			if (ls != null && ls.length == 0)
			    p.delete();
		    }
		}
		vec.removeElement (rent);
		currentSize -= rent.getSize ();
	    }
	    htab.remove (o);			
	}
    }

    /** Clear the Cache from files. 
     */
    public void clear () {
	Enumeration e = htab.keys ();
	while (e.hasMoreElements ()) {
	    remove (e.nextElement ());	    
	}
	currentSize = 0;
	changed = true;
    }

    /** Get the CacheEntries in the cache.
     * @return an Enumeration of the CacheEntries.
     */    
    public Enumeration getEntries () {
	return htab.elements ();
    }

    /** Read the info from an old cache.
     */
    private void readCacheIndex () {
	fileNo = 0;
	currentSize = 0;
	htab = new Hashtable ();
	vec = new Vector ();
	try {
	    String name = dir + File.separator + CACHEINDEX;
	    ObjectInputStream is = 
 		new ObjectInputStream (new FileInputStream (name));
	    fileNo = is.readLong ();
	    dirNo  = is.readLong ();
	    currentSize = is.readLong ();
	    keyHash = (Hashtable)is.readObject();
	    htab = (Hashtable)is.readObject ();
	    vec = (Vector)is.readObject ();
	    is.close ();
	} catch (IOException e) {
	} catch (ClassNotFoundException e) {
	}
    }
    
    /** Make sure that the cache is written to the disk.
     */
    public void flush () {
	writeCacheIndex ();
    }

    /** Store the cache to disk so we can reuse it later.
     */
    private synchronized void writeCacheIndex () {
	try {
	    String name = dir + File.separator + CACHEINDEX;
	    
	    ObjectOutputStream os = 
		new ObjectOutputStream (new FileOutputStream (name));
	    
	    os.writeLong (fileNo);
	    os.writeLong (dirNo);
	    os.writeLong (currentSize);
	    os.writeObject (keyHash);
	    os.writeObject (htab);
	    os.writeObject (vec);
	    os.close ();
	} catch (IOException e) {
	    //	    System.err.println ("Couldnt write " + dir + File.separator + CACHEINDEX + ", This is serious!\n" + e);
	    System.exit (-1);
	}	
    }

    /** Loop in a cleaning loop.
     */
    public void run () {
	setPriority (Thread. MIN_PRIORITY);
	while (true) {
	    try {
		sleep (15000);		
	    } catch (InterruptedException e) {
		//System.err.println ("Cache interrupted");
	    }
	    
	    // actually for a busy cache this will lag...
	    // but I dont care for now... 
	    Date now = new Date ();
	    Enumeration e = htab.elements ();
	    while (e.hasMoreElements ()) {
		NCacheEntry ce = (NCacheEntry) e.nextElement ();
	    }

	    // IF SIZE IS TO BIG REMOVE A RANDOM AMOUNT OF OBJECTS.
	    // What we have to be careful about: we must not remove the same
	    // elements two times in a row, this method remove the "oldest" in 
	    // a sense.

	    if (getCurrentSize () > getMaxSize ())
		changed = true;
	    if (changed) {
		writeCacheIndex ();
		changed = false;
	    }
	}
    }

    /** Configure the cache system from the given config.
     */
    public void setup () {
	String cachedir = "cache";
	setCacheDir (cachedir);
	String cmsize = "10";
	try {
	    setMaxSize (Long.parseLong (cmsize) * 1024 * 1024);     // in MB
	} catch (NumberFormatException e) { 
	}
    }
}
