/*******************************************************************************
 * Copyright (c) 2000, 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.core.internal.indexing;

import java.util.*;

public class ObjectStore implements Observer {

	public static final int MAXIMUM_OBJECT_SIZE = ObjectPage.ObjectSpaceSize - ObjectHeader.SIZE;
	protected static final int CurrentObjectStoreVersion = 1;
	protected static final int ObjectStoreMetadataAreaID = 1;

	protected PageStore pageStore; /* initialized in open */
	protected String name; /* initialized in open */
	protected Map acquiredObjects; // objects which are currently active
	protected Map modifiedObjects; // objects that have been modified since the last sync point.
	protected LinkedList cachedObjects; // objects that are just hanging around waiting to be acquired.  Kept for performance.
	protected Set phantoms; // the set of objects that needs to be deleted since the last sync point.
	protected ReservationTable reservations;

	protected ObjectStorePagePolicy pagePolicy;
	protected AbstractObjectPolicy objectPolicy;

	/**
	 * Creates a repository for the pathname.  
	 */
	public static void create(String path) throws ObjectStoreException {
		try {
			PageStore.create(path);
		} catch (PageStoreException e) {
			throw new ObjectStoreException(ObjectStoreException.StoreCreateFailure, e);
		}
	}

	/**
	 * Deletes the underlying page store.
	 */
	public static void delete(String path) {
		PageStore.delete(path);
	}

	/**
	 * Checks for existence of an object store file.
	 */
	public static boolean exists(String path) {
		return PageStore.exists(path);
	}

	/**
	 * Creates an object store.  This store is unusable until opened.
	 */
	public ObjectStore(AbstractObjectPolicy objectPolicy) {
		this.pagePolicy = new ObjectStorePagePolicy();
		this.objectPolicy = objectPolicy;
	}

	/**
	 * Opens an object store.
	 */
	public void open(String name) throws ObjectStoreException {
		try {
			pageStore = new PageStore(pagePolicy);
			pageStore.open(name);
		} catch (PageStoreException e) {
			throw new ObjectStoreException(ObjectStoreException.StoreOpenFailure, e);
		}
		checkMetadata();
		acquiredObjects = new HashMap();
		modifiedObjects = new HashMap();
		cachedObjects = new LinkedList();
		phantoms = new HashSet();
		reservations = new ReservationTable();
	}

	/**
	 * Closes the object store.
	 */
	public void close() throws ObjectStoreException {
		try {
			commit();
		} catch (ObjectStoreException e) {
			//make sure the page store file gets closed no matter what
			pageStore.close(false);
			throw e;
		}
		try {
			pageStore.close();
		} catch (Exception e) {
			//ignore failure to close
		}
		acquiredObjects = null;
		modifiedObjects = null;
		cachedObjects = null;
		phantoms = null;
		reservations = null;
	}

	public Buffer getMetadataArea(int i) throws ObjectStoreException {
		try {
			return new Buffer(pageStore.readMetadataArea(i));
		} catch (PageStoreException e) {
			throw new ObjectStoreException(ObjectStoreException.MetadataRequestFailure, e);
		}
	}

	public void putMetadataArea(int i, Buffer buffer) throws ObjectStoreException {
		try {
			pageStore.writeMetadataArea(i, buffer.getByteArray());
		} catch (PageStoreException e) {
			throw new ObjectStoreException(ObjectStoreException.MetadataRequestFailure, e);
		}
	}

	/**
	 * Checks to see if the metadata stored in the object store matches that expected by this
	 * code.  If not, a conversion is necessary.
	 */
	protected void checkMetadata() throws ObjectStoreException {
		Buffer metadata = getMetadataArea(ObjectStoreMetadataAreaID);
		Field versionField = metadata.getField(0, 4);
		int objectStoreVersion = versionField.getInt();
		if (objectStoreVersion == 0) {
			// 0 indicates that the store is new and opened for read/write
			versionField.put(CurrentObjectStoreVersion);
			putMetadataArea(ObjectStoreMetadataAreaID, metadata);
			return;
		}
		if (objectStoreVersion == CurrentObjectStoreVersion)
			return;
		convert(objectStoreVersion);
	}

	/**
	 * Converts the object store from a previous to the current version.  
	 * No conversions are yet defined.
	 */
	protected void convert(int fromVersion) throws ObjectStoreException {
		throw new ObjectStoreException(ObjectStoreException.StoreConversionFailure);
	}

	/**
	 * Commits the modified object collection to the underlying page store.
	 */
	public void commit() throws ObjectStoreException {
		for (Iterator z = acquiredObjects.values().iterator(); z.hasNext();) {
			StoredObject object = (StoredObject) z.next();
			object.notifyObservers();
		}
		for (Iterator z = phantoms.iterator(); z.hasNext();) {
			ObjectAddress address = (ObjectAddress) z.next();
			int pageNumber = address.getPageNumber();
			ObjectPage page = acquireObjectPage(pageNumber);
			page.removeObject(address.getObjectNumber());
			updateSpaceMapPage(page.getPageNumber(), page.getFreeSpace());
			page.release();
		}
		phantoms.clear();
		for (Iterator z = modifiedObjects.values().iterator(); z.hasNext();) {
			StoredObject object = (StoredObject) z.next();
			z.remove();
			addToCache(object);
			ObjectAddress address = object.getAddress();
			int pageNumber = address.getPageNumber();
			ObjectPage page = acquireObjectPage(pageNumber);
			if (reservations.contains(address)) {
				page.insertObject(object);
				updateSpaceMapPage(pageNumber, page.getFreeSpace());
			} else {
				page.updateObject(object);
			}
			page.release();
		}
		reservations.clear();
		try {
			pageStore.commit();
		} catch (PageStoreException e) {
			throw new ObjectStoreException(ObjectStoreException.PageWriteFailure, e);
		}
	}

	/**
	 * Returns the StoredObject at a given address.  This registers the store as an 
	 * observer of changes to this object.
	 */
	public StoredObject acquireObject(ObjectAddress address) throws ObjectStoreException {
		if (phantoms.contains(address)) {
			throw new ObjectStoreException(ObjectStoreException.ObjectExistenceFailure);
		}
		StoredObject object = (StoredObject) acquiredObjects.get(address);
		if (object == null) {
			object = (StoredObject) modifiedObjects.get(address);
			if (object == null) {
				object = removeFromCache(address);
				if (object == null) {
					int pageNumber = address.getPageNumber();
					ObjectPage page = acquireObjectPage(pageNumber);
					try {
						Field f = page.getObjectField(address.getObjectNumber());
						if (f == null)
							throw new ObjectStoreException(ObjectStoreException.ObjectExistenceFailure);
						object = objectPolicy.createObject(f, this, address);
					} catch (ObjectStoreException e) {
						page.release();
						throw e;
					}
					page.release();
				}
			}
			acquiredObjects.put(address, object);
			object.addObserver(this);
		}
		object.addReference();
		return object;
	}

	/**
	 * Releases an object.  If there are no more references and the object is not
	 * in the modified cache, return the object to 
	 * the standard cache.  Objects in the standard cache always maintain a 
	 * reference count of 0.
	 */
	public void releaseObject(StoredObject object) throws ObjectStoreException {
		object.removeReference();
		if (object.hasReferences())
			return;
		object.notifyObservers(); // one last chance to collect changed objects
		object.deleteObserver(this);
		acquiredObjects.remove(object.getAddress());
		addToCache(object);
	}

	/**
	 * Updates the store when receiving an object change notification.  Required by
	 * Observer.  This places the object in the modified objects cache.  This will
	 * be cleared during commit/rollback processing.  An object may reside simultaneously
	 * in the modified and acquired object caches.
	 */
	public void update(Observable object, Object arg) {
		StoredObject storedObject = (StoredObject) object;
		modifiedObjects.put(storedObject.getAddress(), storedObject);
	}

	/**
	 * Adds an object to the backing cache.  Objects in this cache are neither in 
	 * the modified set or the acquired set.
	 */
	protected void addToCache(StoredObject object) {
		synchronized (cachedObjects) {
			if (acquiredObjects.containsKey(object.getAddress()))
				return;
			if (modifiedObjects.containsKey(object.getAddress()))
				return;
			cachedObjects.addFirst(object);
			if (cachedObjects.size() <= 50)
				return;
			cachedObjects.removeLast();
		}
	}

	/**
	 * Removes an object from the backing cache given its address.
	 */
	protected StoredObject removeFromCache(ObjectAddress address) {
		synchronized (cachedObjects) {
			StoredObject object = null;
			for (Iterator z = cachedObjects.iterator(); z.hasNext();) {
				StoredObject o = (StoredObject) z.next();
				if (o.getAddress().equals(address)) {
					z.remove();
					object = o;
					break;
				}
			}
			return object;
		}
	}

	/**
	 * Places an object into the store.  This assigns it an address.  The address
	 * is returned. The object is not observed until it is acquired.
	 */
	//	public ObjectAddress insertObject(StoredObject object) throws ObjectStoreException {
	//		int bytesNeeded = object.length() + ObjectHeader.SIZE;
	//		ObjectPage page = acquireObjectPageForSize(bytesNeeded);
	//		int objectNumber = page.insertObject(object);
	//		int pageNumber = page.getPageNumber();
	//		updateSpaceMapPage(page.getPageNumber(), page.getFreeSpace());
	//		page.release();
	//		object.setAddress(new ObjectAddress(pageNumber, objectNumber));
	//		object.setStore(this);
	//		return object.getAddress();
	//	}
	/**
	 * "Inserts" an object into the store by reserving a place for the 
	 * object.  This assigns it an address and
	 * places it in the modified objects map.  A reservation is created that 
	 * records the address and the amount of space used.  The object is not
	 * actually added to the underlying store until a commit operation is executed.
	 */
	public ObjectAddress insertObject(StoredObject object) throws ObjectStoreException {
		int bytesNeeded = object.length() + ObjectHeader.SIZE;
		ObjectPage page = acquireObjectPageForSize(bytesNeeded);
		int pageNumber = page.getPageNumber();
		int objectNumber = page.reserveObject(object, reservations);
		page.release();
		ObjectAddress address = new ObjectAddress(pageNumber, objectNumber);
		object.setAddress(address);
		object.setStore(this);
		modifiedObjects.put(address, object);
		return address;
	}

	/**
	 * Removes an object from the object store.  In doing so, it must remove it from the cache as well.
	 */
	public void removeObject(ObjectAddress address) throws ObjectStoreException {
		if (phantoms.contains(address)) {
			throw new ObjectStoreException(ObjectStoreException.ObjectExistenceFailure);
		}
		if (acquiredObjects.containsKey(address)) {
			throw new ObjectStoreException(ObjectStoreException.ObjectIsLocked);
		}
		StoredObject object = (StoredObject) modifiedObjects.get(address);
		boolean inStore = !reservations.contains(address);
		if (object != null) {
			reservations.remove(address);
			modifiedObjects.remove(address);
		}
		removeFromCache(address);
		if (inStore)
			phantoms.add(address);
	}

	/**
	 * Places the object in the modified objects cache and marks it as in use.  The modified objects
	 * cache is flushed at commit or rollback time.  At that point, the reference to the object
	 * is dropped.
	 */
	//	public void updateObject(StoredObject object) {
	//		ObjectAddress address = object.getAddress();
	//		if (modifiedObjects.get(address) == null) {
	//			modifiedObjects.put(address, object);
	//			try {
	//				acquireObject(address);
	//			} catch (ObjectStoreException e) {
	//			}
	//		}
	//	}

	protected void updateSpaceMapPage(int objectPageNumber, int freeSpace) throws ObjectStoreException {
		SpaceMapPage p = acquireSpaceMapPage(objectPageNumber);
		p.setFreeSpace(objectPageNumber, freeSpace);
		p.release();
	}

	/** 
	 * Acquires an object page.  This is a convenience method to translate exceptions.
	 */
	protected ObjectPage acquireObjectPage(int pageNumber) throws ObjectStoreException {
		ObjectPage page;
		try {
			page = (ObjectPage) pageStore.acquire(pageNumber);
		} catch (PageStoreException e) {
			throw new ObjectStoreException(ObjectStoreException.PageReadFailure, e);
		}
		return page;
	}

	/** 
	 * Looks for the first page that guarantees enough space to meet the criteria.
	 * This is the "first fit" algorithm and will get slow as the page file grows since
	 * it is O(n**2).  (Each addition of a new page is preceded by a search of all pages.)
	 * We reduce the overhead by maintaining SpaceMapPages that tell us how full each page
	 * is.  A space map page is the first page of a span of 8K pages (64M total).
	 * Each byte in a space map page indicates the fullness of each page in the span.
	 * Since databases are expected to be quite small (<200Mb) we might be able to live with
	 * this simple algorithm.
	 */
	protected ObjectPage acquireObjectPageForSize(int bytesNeeded) throws ObjectStoreException {
		int oPageNumber = 0;
		int numberOfSpans = ((pageStore.numberOfPages() - 1) / ObjectStorePage.SIZE) + 1;
		for (int i = 0; i <= numberOfSpans; i++) {
			try {
				int sPageNumber = i * ObjectStorePage.SIZE;
				SpaceMapPage sPage = (SpaceMapPage) pageStore.acquire(sPageNumber);
				for (int j = 1; j < ObjectStorePage.SIZE; j++) {
					int n = sPageNumber + j;
					Reservation r = reservations.get(n);
					int bytesAvailable = (r == null) ? sPage.getFreeSpace(n) : r.getFreeSpace();
					if (bytesNeeded <= bytesAvailable) {
						oPageNumber = n;
						break;
					}
				}
				sPage.release();
			} catch (PageStoreException e) {
				throw new ObjectStoreException(ObjectStoreException.PageReadFailure, e);
			}
			if (oPageNumber != 0)
				break;
		}
		if (oPageNumber == 0) {
			throw new ObjectStoreException(ObjectStoreException.PageReadFailure);
		}
		try {
			ObjectPage oPage = (ObjectPage) pageStore.acquire(oPageNumber);
			return oPage;
		} catch (PageStoreException e) {
			throw new ObjectStoreException(ObjectStoreException.PageReadFailure, e);
		}
	}

	/** 
	 * Acquires a space map page.  This is a convenience method to translate exceptions.
	 */
	protected SpaceMapPage acquireSpaceMapPage(int objectPageNumber) throws ObjectStoreException {
		int pageNumber = objectPageNumber & 0xFFFFE000;
		SpaceMapPage p = null;
		try {
			p = (SpaceMapPage) pageStore.acquire(pageNumber);
		} catch (PageStoreException e) {
			throw new ObjectStoreException(ObjectStoreException.PageReadFailure, e);
		}
		return p;
	}

}
