// Copyright (c) 1999 Dustin Sallings <dustin@spy.net>

package net.spy.cache;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.ref.SoftReference;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.AbstractMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import net.spy.log.Logger;
import net.spy.log.LoggerFactory;
import net.spy.util.CloseUtil;
import net.spy.util.SpyUtil;

/**
 * Simple local disk caching.
 *
 * This is used for terribly simple caches with no expiration dates on
 * objects.  Things go in and they stay in.
 */
public class DiskCache extends AbstractMap<Serializable, Serializable> {

	// Base directory for hashing
	final String basedir;
	private final LRUCache<Serializable, SoftReference<Serializable>> lruCache;

	private static final int DEFAULT_LRU_CACHE_SIZE=100;

	private transient Logger logger = null;

	/**
	 * Get an DiskObject using the given directory.
	 */
	public DiskCache(String base) {
		this(base, DEFAULT_LRU_CACHE_SIZE);
	}

	/**
	 * Get a DiskCache using the given directory with a backing LRU cache
	 * of the specified size.
	 *
	 * @param base the base directory for the disk cache
	 * @param lruCacheSize the size of the LRU cache holding recently accessed
	 *		objects
	 */
	public DiskCache(String base, int lruCacheSize) {
		super();
		this.basedir=base;
		lruCache=new LRUCache<Serializable,
			SoftReference<Serializable>>(lruCacheSize);
	}

	/**
	 * Get the base directory which this cache is watching.
	 */
	public String getBaseDir() {
		return(basedir);
	}

	// Get the path of an object for the given key
	private String getPath(Object key) {
		MessageDigest md=null;
		try {
			md=MessageDigest.getInstance("SHA");
		} catch(NoSuchAlgorithmException e) {
			throw new AssertionError("There's no SHA?");
		}
		if(key instanceof String) {
			String k=(String)key;
			md.update(k.getBytes());
		} else {
			String tmpkey=key.getClass().getName() + key.hashCode();
			md.update(tmpkey.getBytes());
		}

		String hashed=SpyUtil.byteAToHexString(md.digest());

		String base=basedir+"/"+hashed.substring(0,2);
		String path=base+"/"+hashed;

		File f=new File(base);
		if(!f.isDirectory()) {
			f.mkdirs();
		}

		return(path);
	}

	/**
	 * Store an object in the cache.
	 *
     * @param k object key
     * @param v value
     * @return the old object stored in that location (if any)
	 */
    @Override
	public Serializable put(Serializable k, Serializable v) {
		Serializable rv=get(k);

		String pathto=getPath(k);

		FileOutputStream ostream=null;
		ObjectOutputStream p=null;
		try {
			ostream = new FileOutputStream(pathto);
			p = new ObjectOutputStream(ostream);
			p.writeObject(k);
			p.writeObject(v);
			p.flush();
		} catch(IOException e) {
			throw new RuntimeException("Error storing object", e);
		} finally {
			CloseUtil.close(p);
			CloseUtil.close(ostream);
		}

		return(rv);
	}

	/**
	 * Get an object from the cache.
	 *
	 * @return the object, or null if there's no such object
	 */
    @Override
	public Serializable get(Object key) {
		Serializable rv=null;

		if(key==null) {
			throw new NullPointerException("Name not provided");
		}

		rv=(Serializable) lruCache.get(key);
		if(rv==null) {
			rv=(Serializable) getFromDiskCache(key);
			lruCache.put((Serializable) key,
					new SoftReference<Serializable>(rv));
		}

		return(rv);
	}

	private Logger getLogger() {
		if(logger==null) {
			logger=LoggerFactory.getLogger(getClass());
		}
		return(logger);
	}

	private Object getFromDiskCache(Object key) {

		Object rv=null;

		FileInputStream istream=null;
		ObjectInputStream p=null;
		try {
			istream = new FileInputStream(getPath(key));
			p = new ObjectInputStream(istream);
			Object storedKey = p.readObject();
			Object o = p.readObject();

			if(!key.equals(storedKey)) {
				throw new Exception("Key value did not match ("
					+ storedKey + " != " + key + ")");
			}

			rv=o;
		} catch(FileNotFoundException e) {
			if(getLogger().isDebugEnabled()) {
				getLogger().debug("File not found loading disk cache", e);
			}
		} catch(Exception e) {
			getLogger().warn("Error getting ``%s'' from disk cache", key, e);
		} finally {
			CloseUtil.close(p);
			CloseUtil.close(istream);
		}

		return(rv);
	}

	@Override
	public Set<Map.Entry<Serializable, Serializable>> entrySet() {
		Set<Map.Entry<Serializable, Serializable>> rv=null;

		try {
			rv=new WalkerDiskCacheRanger();
		} catch(ClassNotFoundException e) {
			throw new RuntimeException("Error getting set", e);
		} catch(IOException e) {
			throw new RuntimeException("Error getting set", e);
		}

		return(rv);
	}

	private class WalkerDiskCacheRanger
		extends HashSet<Map.Entry<Serializable, Serializable>> {

		public WalkerDiskCacheRanger()
			throws IOException, ClassNotFoundException {
			super();

			init(new File(basedir));
		}

		private void init(File f) throws IOException, ClassNotFoundException {
			if(f.isDirectory()) {
				// If it's a directory, recurse
				File[] stuff=f.listFiles();
				for(int i=0; i<stuff.length; i++) {
					init(stuff[i]);
				}
			} else {
				// Regular file, open it and read it
				FileInputStream istream = null;
				ObjectInputStream p = null;
				Serializable key=null;
				try {
					istream = new FileInputStream(f);
					p = new ObjectInputStream(istream);
					key=(Serializable) p.readObject();
				} finally {
					CloseUtil.close(p);
					CloseUtil.close(istream);
				}

				// Add the new entry.
				add(new E(key, f));
			}
		}

		/**
		 * Get an iterator.
		 */
		@Override
		public Iterator<Entry<Serializable, Serializable>> iterator() {
			return(new I(super.iterator()));
		}

	}

	// Iterator implementation
	private static class I extends
		Object implements Iterator<Entry<Serializable, Serializable>> {

		private final Iterator<Map.Entry<Serializable, Serializable>> i;
		private E current=null;
		private boolean begun=false;

		// Instatiate the iterator over the default iterator implementation
		public I(Iterator<Map.Entry<Serializable, Serializable>> it) {
			super();
			i=it;
		}

		/**
		 * Get the next object.
		 */
		public E next() {
			begun=true;
			current=(E) i.next();
			return(current);
		}

		/**
		 * True if there's another entry.
		 */
		public boolean hasNext() {
			return(i.hasNext());
		}

		/**
		 * Remove the given object.
		 */
		public void remove() {
			if(begun==false) {
				throw new IllegalStateException("Have not yet begun walking.");
			}

			File f=current.getPath();
			f.delete();
			i.remove();
		}

	}

	// Map entry implementation
	private static class E extends Object
		implements Map.Entry<Serializable, Serializable> {

		final File path;
		final Serializable k;

		public E(Serializable key, File p) {
			super();
			k=key;
			path=p;
		}

		/**
		 * True if two objects are equal.
		 */
		@Override
		public boolean equals(Object o) {
			boolean rv=false;

			if(o instanceof E) {
				E e=(E)o;
				rv=e.k.equals(k);
			}

			return(rv);
		}

		/**
		 * Get the path to the file.
		 */
		public File getPath() {
			return(path);
		}

		/**
		 * Get the key.
		 */
		public Serializable getKey() {
			return(k);
		}

		/**
		 * Get the hash code.
		 */
		@Override
		public int hashCode() {
			return(k.hashCode());
		}

		/**
		 * Get the value.
		 */
		public Serializable getValue() {
			Serializable rv=null;

			FileInputStream istream=null;
			ObjectInputStream p=null;
			try {
				istream = new FileInputStream(path);
				p = new ObjectInputStream(istream);
				Object key=p.readObject();
				assert(key.equals(k));
				rv=(Serializable) p.readObject();
			} catch(IOException e) {
				throw new RuntimeException("Error getting object",e);
			} catch(ClassNotFoundException e) {
				throw new RuntimeException("Error getting object",e);
			} finally {
				CloseUtil.close(p);
				CloseUtil.close(istream);
			}

			return(rv);
		}

		/**
		 * Not implemented.
		 */
		public Serializable setValue(Serializable o) {
			throw new UnsupportedOperationException("Can't set here.");
		}

	}

}
