/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.client;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;

import freenet.client.async.ClientContext;
import freenet.keys.FreenetURI;
import freenet.support.ExceptionWrapper;
import freenet.support.LRUMap;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.MutableBoolean;
import freenet.support.api.Bucket;
import freenet.support.api.BucketFactory;
import freenet.support.compress.CompressionOutputSizeException;
import freenet.support.compress.Compressor;
import freenet.support.compress.Compressor.COMPRESSOR_TYPE;
import freenet.support.io.BucketTools;
import freenet.support.io.Closer;
import freenet.support.io.SkipShieldingInputStream;
import net.contrapunctus.lzma.LzmaInputStream;

/**
 * Cache of recently decoded archives:
 * - Keep up to N ArchiveHandler's in RAM (this can be large; we don't keep the
 * files open due to the limitations of the java.util.zip API)
 * - Keep up to Y bytes (after padding and overheads) of decoded data on disk
 * (the OS is quite capable of determining what to keep in actual RAM)
 *
 * Always take the lock on ArchiveStoreContext before the lock on ArchiveManager, NOT the other way around.
 */
public class ArchiveManager {

	public static final String METADATA_NAME = ".metadata";
	private static boolean logMINOR;

	public enum ARCHIVE_TYPE {
	    // WARNING: This enum is persisted. Changing member names may break downloads/uploads.
		ZIP((short)0, new String[] { "application/zip", "application/x-zip" }), 	/* eventually get rid of ZIP support at some point */
		TAR((short)1, new String[] { "application/x-tar" });

		public final short metadataID;
		public final String[] mimeTypes;

		/** cached values(). Never modify or pass this array to outside code! */
		private static final ARCHIVE_TYPE[] values = values();

		private ARCHIVE_TYPE(short metadataID, String[] mimeTypes) {
			this.metadataID = metadataID;
			this.mimeTypes = mimeTypes;
		}

		public static boolean isValidMetadataID(short id) {
			for(ARCHIVE_TYPE current : values)
				if(id == current.metadataID)
					return true;
			return false;
		}

		/**
		 * Is the given MIME type an archive type that we can deal with?
		 */
		public static boolean isUsableArchiveType(String type) {
			for(ARCHIVE_TYPE current : values)
				for(String ctype : current.mimeTypes)
					if(ctype.equalsIgnoreCase(type))
						return true;
			return false;
		}

		/** If the given MIME type is an archive type that we can deal with,
		 * get its archive type number (see the ARCHIVE_ constants in Metadata).
		 */
		public static ARCHIVE_TYPE getArchiveType(String type) {
			for(ARCHIVE_TYPE current : values)
				for(String ctype : current.mimeTypes)
					if(ctype.equalsIgnoreCase(type))
						return current;
			return null;
		}

		public static ARCHIVE_TYPE getArchiveType(short type) {
			for(ARCHIVE_TYPE current : values)
				if(current.metadataID == type)
					return current;
			return null;
		}

		public static ARCHIVE_TYPE getDefault() {
			return TAR;
		}
	}

	final long maxArchivedFileSize;

	// ArchiveHandler's
	final int maxArchiveHandlers;
	private final LRUMap<FreenetURI, ArchiveStoreContext> archiveHandlers;

	// Data cache
	/** Maximum number of cached ArchiveStoreItems */
	final int maxCachedElements;
	/** Maximum cached data in bytes */
	final long maxCachedData;
	/** Currently cached data in bytes */
	private long cachedData;
	/** Map from ArchiveKey to ArchiveStoreElement */
	private final LRUMap<ArchiveKey, ArchiveStoreItem> storedData;
	/** Bucket Factory */
	private final BucketFactory tempBucketFactory;

	/**
	 * Create an ArchiveManager.
	 * @param maxHandlers The maximum number of cached ArchiveHandler's i.e. the
	 * maximum number of containers to track.
	 * @param maxCachedData The maximum size of the cache directory, in bytes.
	 * @param maxArchiveSize The maximum size of an archive.
	 * @param maxArchivedFileSize The maximum extracted size of a single file in any
	 * archive.
	 * @param maxCachedElements The maximum number of cached elements (an element is a
	 * file extracted from an archive. It is stored, encrypted and padded, in a single
	 * file.
	 * @param tempBucketFactory
	 */
	public ArchiveManager(int maxHandlers, long maxCachedData, long maxArchivedFileSize, int maxCachedElements, BucketFactory tempBucketFactory) {
		maxArchiveHandlers = maxHandlers;
		// FIXME PERFORMANCE I'm assuming there isn't much locality here, so it's faster to use the FAST_COMPARATOR.
		// This may not be true if there are a lot of sites with many containers all inserted as individual SSKs?
		archiveHandlers = LRUMap.createSafeMap(FreenetURI.FAST_COMPARATOR);
		this.maxCachedElements = maxCachedElements;
		this.maxCachedData = maxCachedData;
		storedData = new LRUMap<ArchiveKey, ArchiveStoreItem>();
		this.maxArchivedFileSize = maxArchivedFileSize;
		this.tempBucketFactory = tempBucketFactory;
		logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
	}

	/** Add an ArchiveHandler by key */
	private synchronized void putCached(FreenetURI key, ArchiveStoreContext zip) {
		if(logMINOR) Logger.minor(this, "Put cached AH for "+key+" : "+zip);
		archiveHandlers.push(key, zip);
		while(archiveHandlers.size() > maxArchiveHandlers)
			archiveHandlers.popKey(); // dump it
	}

	/** Get an ArchiveHandler by key */
	ArchiveStoreContext getCached(FreenetURI key) {
		if(logMINOR) Logger.minor(this, "Get cached AH for "+key);
		ArchiveStoreContext handler = archiveHandlers.get(key);
		if(handler == null) return null;
		archiveHandlers.push(key, handler);
		return handler;
	}

	/**
	 * Create an archive handler. This does not need to know how to
	 * fetch the key, because the methods called later will ask.
	 * It will try to serve from cache, but if that fails, will
	 * re-fetch.
	 * @param key The key of the archive that we are extracting data from.
	 * @param archiveType The archive type, defined in Metadata.
	 * @return An archive handler.
	 */
	synchronized ArchiveStoreContext makeContext(FreenetURI key, ARCHIVE_TYPE archiveType, COMPRESSOR_TYPE ctype, boolean returnNullIfNotFound) {
		ArchiveStoreContext handler = null;
		handler = getCached(key);
		if(handler != null) return handler;
		if(returnNullIfNotFound) return null;
		handler = new ArchiveStoreContext(key, archiveType);
		putCached(key, handler);
		return handler;
	}

	/**
	 * Create an archive handler. This does not need to know how to
	 * fetch the key, because the methods called later will ask.
	 * It will try to serve from cache, but if that fails, will
	 * re-fetch.
	 * @param key The key of the archive that we are extracting data from.
	 * @param archiveType The archive type, defined in Metadata.
	 * @return An archive handler.
	 */
	public ArchiveHandler makeHandler(FreenetURI key, ARCHIVE_TYPE archiveType, COMPRESSOR_TYPE ctype, boolean forceRefetch, boolean persistent) {
		return new ArchiveHandlerImpl(key, archiveType, ctype, forceRefetch);
	}

	/**
	 * Get a cached, previously extracted, file from an archive.
	 * @param key The key used to fetch the archive.
	 * @param filename The name of the file within the archive.
	 * @return A Bucket containing the data requested, or null.
	 * @throws ArchiveFailureException
	 */
	public Bucket getCached(FreenetURI key, String filename) throws ArchiveFailureException {
		if(logMINOR) Logger.minor(this, "Fetch cached: "+key+ ' ' +filename);
		ArchiveKey k = new ArchiveKey(key, filename);
		ArchiveStoreItem asi = null;
		synchronized (this) {
			asi = storedData.get(k);
			if(asi == null) return null;
			// Promote to top of LRU
			storedData.push(k, asi);
		}
		if(logMINOR) Logger.minor(this, "Found data");
		return asi.getReaderBucket();
	}

	/**
	 * Remove a file from the cache. Called after it has been removed from its
	 * ArchiveHandler.
	 * @param item The ArchiveStoreItem to remove.
	 */
	synchronized void removeCachedItem(ArchiveStoreItem item) {
		long size = item.spaceUsed();
		storedData.removeKey(item.key);
		// Hard disk space limit = remove it here.
		// Soft disk space limit would be to remove it outside the lock.
		// Soft disk space limit = we go over the limit significantly when we
		// are overloaded.
		cachedData -= size;
		if(logMINOR) Logger.minor(this, "removeCachedItem: "+item);
		item.close();
	}

	/**
	 * Extract data to cache. Call synchronized on ctx.
	 * @param key The key the data was fetched from.
	 * @param archiveType The archive type. Must be Metadata.ARCHIVE_ZIP | Metadata.ARCHIVE_TAR.
	 * @param data The actual data fetched.
	 * @param archiveContext The context for the whole fetch process.
	 * @param ctx The ArchiveStoreContext for this key.
	 * @param element A particular element that the caller is especially interested in, or null.
	 * @param callback A callback to be called if we find that element, or if we don't.
	 * @throws ArchiveFailureException If we could not extract the data, or it was too big, etc.
	 * @throws ArchiveRestartException
	 * @throws ArchiveRestartException If the request needs to be restarted because the archive
	 * changed.
	 */
	public void extractToCache(FreenetURI key, ARCHIVE_TYPE archiveType, COMPRESSOR_TYPE ctype, final Bucket data, ArchiveContext archiveContext, ArchiveStoreContext ctx, String element, ArchiveExtractCallback callback, ClientContext context) throws ArchiveFailureException, ArchiveRestartException {
		logMINOR = Logger.shouldLog(LogLevel.MINOR, this);

		MutableBoolean gotElement = element != null ? new MutableBoolean() : null;

		if(logMINOR) Logger.minor(this, "Extracting "+key);
		ctx.removeAllCachedItems(this); // flush cache anyway
		final long expectedSize = ctx.getLastSize();
		final long archiveSize = data.size();
		/** Set if we need to throw a RestartedException rather than returning success,
		 * after we have unpacked everything.
		 */
		boolean throwAtExit = false;
		if((expectedSize != -1) && (archiveSize != expectedSize)) {
			throwAtExit = true;
			ctx.setLastSize(archiveSize);
		}
		byte[] expectedHash = ctx.getLastHash();
		if(expectedHash != null) {
			byte[] realHash;
			try {
				realHash = BucketTools.hash(data);
			} catch (IOException e) {
				throw new ArchiveFailureException("Error reading archive data: "+e, e);
			}
			if(!Arrays.equals(realHash, expectedHash))
				throwAtExit = true;
			ctx.setLastHash(realHash);
		}

		if(archiveSize > archiveContext.maxArchiveSize)
			throw new ArchiveFailureException("Archive too big ("+archiveSize+" > "+archiveContext.maxArchiveSize+")!");
		else if(archiveSize <= 0)
			throw new ArchiveFailureException("Archive too small! ("+archiveSize+')');
		else if(logMINOR)
			Logger.minor(this, "Container size (possibly compressed): "+archiveSize+" for "+data);

		InputStream is = null;
		try {
			final ExceptionWrapper wrapper;
			if((ctype == null) || (ARCHIVE_TYPE.ZIP == archiveType)) {
				if(logMINOR) Logger.minor(this, "No compression");
				is = data.getInputStream();
				wrapper = null;
			} else if(ctype == COMPRESSOR_TYPE.BZIP2) {
				if(logMINOR) Logger.minor(this, "dealing with BZIP2");
				is = new BZip2CompressorInputStream(data.getInputStream());
				wrapper = null;
			} else if(ctype == COMPRESSOR_TYPE.GZIP) {
				if(logMINOR) Logger.minor(this, "dealing with GZIP");
				is = new GZIPInputStream(data.getInputStream());
				wrapper = null;
			} else if(ctype == COMPRESSOR_TYPE.LZMA_NEW) {
				// LZMA internally uses pipe streams, so we may as well do it here.
				// In fact we need to for LZMA_NEW, because of the properties bytes.
				PipedInputStream pis = new PipedInputStream();
				PipedOutputStream pos = new PipedOutputStream();
				pis.connect(pos);
				final OutputStream os = new BufferedOutputStream(pos);
				wrapper = new ExceptionWrapper();
				context.mainExecutor.execute(new Runnable() {

					@Override
					public void run() {
						InputStream is = null;
						try {
							Compressor.COMPRESSOR_TYPE.LZMA_NEW.decompress(is = data.getInputStream(), os, data.size(), expectedSize);
						} catch (CompressionOutputSizeException e) {
							Logger.error(this, "Failed to decompress archive: "+e, e);
							wrapper.set(e);
						} catch (IOException e) {
							Logger.error(this, "Failed to decompress archive: "+e, e);
							wrapper.set(e);
						} finally {
							try {
								os.close();
							} catch (IOException e) {
								Logger.error(this, "Failed to close PipedOutputStream: "+e, e);
							}
							Closer.close(is);
						}
					}
					
				});
				is = pis;
			} else if(ctype == COMPRESSOR_TYPE.LZMA) {
				if(logMINOR) Logger.minor(this, "dealing with LZMA");
				is = new LzmaInputStream(data.getInputStream());
				wrapper = null;
			} else {
				wrapper = null;
			}

			if(ARCHIVE_TYPE.ZIP == archiveType) {
				handleZIPArchive(ctx, key, is, element, callback, gotElement, throwAtExit, context);
			} else if(ARCHIVE_TYPE.TAR == archiveType) {
				 // COMPRESS-449 workaround, see https://freenet.mantishub.io/view.php?id=6921
				handleTARArchive(ctx, key, new SkipShieldingInputStream(is), element, callback, gotElement, throwAtExit, context);
			} else {
				throw new ArchiveFailureException("Unknown or unsupported archive algorithm " + archiveType);
			}
			if(wrapper != null) {
				Exception e = wrapper.get();
				if(e != null) throw new ArchiveFailureException("An exception occured decompressing: "+e.getMessage(), e);
			}
		} catch (IOException ioe) {
			throw new ArchiveFailureException("An IOE occured: "+ioe.getMessage(), ioe);
		} finally {
			Closer.close(is);
	}
	}

	private void handleTARArchive(ArchiveStoreContext ctx, FreenetURI key, InputStream data, String element, ArchiveExtractCallback callback, MutableBoolean gotElement, boolean throwAtExit, ClientContext context) throws ArchiveFailureException, ArchiveRestartException {
		if(logMINOR) Logger.minor(this, "Handling a TAR Archive");
		TarArchiveInputStream tarIS = null;
		try {
			tarIS = new TarArchiveInputStream(data);

			// MINOR: Assumes the first entry in the tarball is a directory.
			ArchiveEntry entry;

			byte[] buf = new byte[32768];
			HashSet<String> names = new HashSet<String>();
			boolean gotMetadata = false;

outerTAR:		while(true) {
				try {
				entry = tarIS.getNextEntry();
				} catch (IllegalArgumentException e) {
					// Annoyingly, it can throw this on some corruptions...
					throw new ArchiveFailureException("Error reading archive: "+e.getMessage(), e);
				}
				if(entry == null) break;
				if(entry.isDirectory()) continue;
				String name = stripLeadingSlashes(entry.getName());
				if(names.contains(name)) {
					Logger.error(this, "Duplicate key "+name+" in archive "+key);
					continue;
				}
				long size = entry.getSize();
				if(name.equals(".metadata"))
					gotMetadata = true;
				if(size > maxArchivedFileSize && !name.equals(element)) {
					addErrorElement(ctx, key, name, "File too big: "+size+" greater than current archived file size limit "+maxArchivedFileSize, true);
				} else {
					// Read the element
					long realLen = 0;
					Bucket output = tempBucketFactory.makeBucket(size);
					OutputStream out = output.getOutputStream();

					try {
						int readBytes;
						while((readBytes = tarIS.read(buf)) > 0) {
							out.write(buf, 0, readBytes);
							readBytes += realLen;
							if(readBytes > maxArchivedFileSize) {
								addErrorElement(ctx, key, name, "File too big: "+maxArchivedFileSize+" greater than current archived file size limit "+maxArchivedFileSize, true);
								out.close();
								out = null;
								output.free();
								continue outerTAR;
							}
						}
						
					} finally {
						if(out != null) out.close();
					}
					if(size <= maxArchivedFileSize) {
						addStoreElement(ctx, key, name, output, gotElement, element, callback, context);
						names.add(name);
						trimStoredData();
					} else {
						// We are here because they asked for this file.
						callback.gotBucket(output, context);
						gotElement.value = true;
						addErrorElement(ctx, key, name, "File too big: "+size+" greater than current archived file size limit "+maxArchivedFileSize, true);
					}
				}
			}

			// If no metadata, generate some
			if(!gotMetadata) {
				generateMetadata(ctx, key, names, gotElement, element, callback, context);
				trimStoredData();
			}
			if(throwAtExit) throw new ArchiveRestartException("Archive changed on re-fetch");

			if((!gotElement.value) && element != null)
				callback.notInArchive(context);

		} catch (IOException e) {
			throw new ArchiveFailureException("Error reading archive: "+e.getMessage(), e);
		} finally {
			Closer.close(tarIS);
		}
	}

	private void handleZIPArchive(ArchiveStoreContext ctx, FreenetURI key, InputStream data, String element, ArchiveExtractCallback callback, MutableBoolean gotElement, boolean throwAtExit, ClientContext context) throws ArchiveFailureException, ArchiveRestartException {
		if(logMINOR) Logger.minor(this, "Handling a ZIP Archive");
		ZipInputStream zis = null;
		try {
			zis = new ZipInputStream(data);

			// MINOR: Assumes the first entry in the zip is a directory.
			ZipEntry entry;

			byte[] buf = new byte[32768];
			HashSet<String> names = new HashSet<String>();
			boolean gotMetadata = false;

outerZIP:		while(true) {
				entry = zis.getNextEntry();
				if(entry == null) break;
				if(entry.isDirectory()) continue;
				String name = stripLeadingSlashes(entry.getName());
				if(names.contains(name)) {
					Logger.error(this, "Duplicate key "+name+" in archive "+key);
					continue;
				}
				long size = entry.getSize();
				if(name.equals(".metadata"))
					gotMetadata = true;
				if(size > maxArchivedFileSize && !name.equals(element)) {
					addErrorElement(ctx, key, name, "File too big: "+maxArchivedFileSize+" greater than current archived file size limit "+maxArchivedFileSize, true);
				} else {
					// Read the element
					long realLen = 0;
					Bucket output = tempBucketFactory.makeBucket(size);
					OutputStream out = output.getOutputStream();
					try {
						
						int readBytes;
						while((readBytes = zis.read(buf)) > 0) {
							out.write(buf, 0, readBytes);
							readBytes += realLen;
							if(readBytes > maxArchivedFileSize) {
								addErrorElement(ctx, key, name, "File too big: "+maxArchivedFileSize+" greater than current archived file size limit "+maxArchivedFileSize, true);
								out.close();
								out = null;
								output.free();
								continue outerZIP;
							}
						}
						
					} finally {
						if(out != null) out.close();
					}
					if(size <= maxArchivedFileSize) {
						addStoreElement(ctx, key, name, output, gotElement, element, callback, context);
						names.add(name);
						trimStoredData();
					} else {
						// We are here because they asked for this file.
						callback.gotBucket(output, context);
						gotElement.value = true;
						addErrorElement(ctx, key, name, "File too big: "+size+" greater than current archived file size limit "+maxArchivedFileSize, true);
					}
				}
			}

			// If no metadata, generate some
			if(!gotMetadata) {
				generateMetadata(ctx, key, names, gotElement, element, callback, context);
				trimStoredData();
			}
			if(throwAtExit) throw new ArchiveRestartException("Archive changed on re-fetch");

			if((!gotElement.value) && element != null)
				callback.notInArchive(context);

		} catch (IOException e) {
			throw new ArchiveFailureException("Error reading archive: "+e.getMessage(), e);
		} finally {
			if(zis != null) {
				try {
					zis.close();
				} catch (IOException e) {
					Logger.error(this, "Failed to close stream: "+e, e);
				}
			}
		}
	}

	private String stripLeadingSlashes(String name) {
		while(name.length() > 1 && name.charAt(0) == '/')
			name = name.substring(1);
		return name;
	}

	/**
	 * Generate fake metadata for an archive which doesn't have any.
	 * @param ctx The context object.
	 * @param key The key from which the archive we are unpacking was fetched.
	 * @param names Set of names in the archive.
	 * @param element2
	 * @param gotElement
	 * @param callbackName If we generate a
	 * @throws ArchiveFailureException
	 */
	private ArchiveStoreItem generateMetadata(ArchiveStoreContext ctx, FreenetURI key, Set<String> names, MutableBoolean gotElement, String element2, ArchiveExtractCallback callback, ClientContext context) throws ArchiveFailureException {
		/* What we have to do is to:
		 * - Construct a filesystem tree of the names.
		 * - Turn each level of the tree into a Metadata object, including those below it, with
		 * simple manifests and archive internal redirects.
		 * - Turn the master Metadata object into binary metadata, with all its subsidiaries.
		 * - Create a .metadata entry containing this data.
		 */
		// Root directory.
		// String -> either itself, or another HashMap
		HashMap<String, Object> dir = new HashMap<String, Object>();
		for (String name : names) {
			addToDirectory(dir, name, "");
		}
		Metadata metadata = new Metadata(dir, "");
		int x = 0;
		Bucket bucket = null;
		while(true) {
			try {
				bucket = metadata.toBucket(tempBucketFactory);
				return addStoreElement(ctx, key, ".metadata", bucket, gotElement, element2, callback, context);
			} catch (MetadataUnresolvedException e) {
				try {
					x = resolve(e, x, tempBucketFactory, ctx, key, gotElement, element2, callback, context);
				} catch (IOException e1) {
					throw new ArchiveFailureException("Failed to create metadata: "+e1, e1);
				}
			} catch (IOException e1) {
				Logger.error(this, "Failed to create metadata: "+e1, e1);
				throw new ArchiveFailureException("Failed to create metadata: "+e1, e1);
			}
		}
	}

	private int resolve(MetadataUnresolvedException e, int x, BucketFactory bf, ArchiveStoreContext ctx, FreenetURI key, MutableBoolean gotElement, String element2, ArchiveExtractCallback callback, ClientContext context) throws IOException, ArchiveFailureException {
		for(Metadata m: e.mustResolve) {
			try {
			    addStoreElement(ctx, key, ".metadata-"+(x++), m.toBucket(bf), gotElement, element2, callback, context);
			} catch (MetadataUnresolvedException e1) {
				x = resolve(e, x, bf, ctx, key, gotElement, element2, callback, context);
				continue;
			}
		}
		return x;
	}

	private void addToDirectory(HashMap<String, Object> dir, String name, String prefix) throws ArchiveFailureException {
		int x = name.indexOf('/');
		if(x < 0) {
			if(dir.containsKey(name)) {
				throw new ArchiveFailureException("Invalid archive: contains "+prefix+name+" twice");
			}
			dir.put(name, name);
		} else {
			String before = name.substring(0, x);
			String after;
			if(x == name.length()-1) {
				// Last char
				after = "";
			} else
				after = name.substring(x+1, name.length());
			Object o = dir.get(before);
			if (o == null) {
				dir.put(before, o = new HashMap<String, Object>());
			} else if (o instanceof String) {
				throw new ArchiveFailureException("Invalid archive: contains "+name+" as both file and dir");
			}
			addToDirectory(Metadata.forceMap(o), after, prefix + before + '/');
		}
	}

	/**
	 * Add an error element to the cache. This happens when a single file in the archive
	 * is invalid (usually because it is too large).
	 * @param ctx The ArchiveStoreContext which must be notified about this element's creation.
	 * @param key The key from which the archive was fetched.
	 * @param name The name of the file within the archive.
	 * @param error The error message to be included on the eventual exception thrown,
	 * if anyone tries to extract the data for this element.
	 */
	private void addErrorElement(ArchiveStoreContext ctx, FreenetURI key, String name, String error, boolean tooBig) {
		ErrorArchiveStoreItem element = new ErrorArchiveStoreItem(ctx, key, name, error, tooBig);
		element.addToContext();
		if(logMINOR) Logger.minor(this, "Adding error element: "+element+" for "+key+ ' ' +name);
		ArchiveStoreItem oldItem;
		synchronized (this) {
			oldItem = storedData.get(element.key);
			storedData.push(element.key, element);
			if(oldItem != null) {
				oldItem.close();
				cachedData -= oldItem.spaceUsed();
				if(logMINOR) Logger.minor(this, "Dropping old store element from archive cache: "+oldItem);
			}
		}
	}

	/**
	 * Add a store element.
	 * @param callbackName If set, the name of the file for which we must call the callback if this file happens to
	 * match.
	 * @param gotElement Flag indicating whether we've already found the file for the callback. If so we must not call
	 * it again.
	 * @param callback Callback to be called if we do find it. We must getReaderBucket() before adding the data to the
	 * LRU, otherwise it may be deleted before it reaches the client.
	 * @throws ArchiveFailureException If a failure occurred resulting in the data not being readable. Only happens if
	 * callback != null.
	 */
	private ArchiveStoreItem addStoreElement(ArchiveStoreContext ctx, FreenetURI key, String name, Bucket temp, MutableBoolean gotElement, String callbackName, ArchiveExtractCallback callback, ClientContext context) throws ArchiveFailureException {
		RealArchiveStoreItem element = new RealArchiveStoreItem(ctx, key, name, temp);
		element.addToContext();
		if(logMINOR) Logger.minor(this, "Adding store element: "+element+" ( "+key+ ' ' +name+" size "+element.spaceUsed()+" )");
		ArchiveStoreItem oldItem;
		// Let it throw, if it does something is drastically wrong
		Bucket matchBucket = null;
		if((!gotElement.value) && name.equals(callbackName)) {
			matchBucket = element.getReaderBucket();
		}
		synchronized (this) {
			oldItem = storedData.get(element.key);
			storedData.push(element.key, element);
			cachedData += element.spaceUsed();
			if(oldItem != null) {
				cachedData -= oldItem.spaceUsed();
				if(logMINOR) Logger.minor(this, "Dropping old store element from archive cache: "+oldItem);
				oldItem.close();
			}
		}
		if(matchBucket != null) {
			callback.gotBucket(matchBucket, context);
			gotElement.value = true;
		}
		return element;
	}

	/**
	 * Drop any stored data beyond the limit.
	 * Call synchronized on storedData.
	 */
	private void trimStoredData() {
		synchronized(this) {
		while(true) {
			ArchiveStoreItem item;
				if(cachedData <= maxCachedData && storedData.size() <= maxCachedElements) return;
				if(storedData.isEmpty()) {
					// Race condition? cachedData out of sync?
					Logger.error(this, "storedData is empty but still over limit: cachedData="+cachedData+" / "+maxCachedData);
					return;
				}
				item = storedData.popValue();
				long space = item.spaceUsed();
				cachedData -= space;
				// Hard limits = delete file within lock, soft limits = delete outside of lock
				// Here we use a hard limit
			if(logMINOR)
				Logger.minor(this, "Dropping "+item+" : cachedData="+cachedData+" of "+maxCachedData+" stored items : "+storedData.size()+" of "+maxCachedElements);
			item.close();
		}
		}
	}

}
