/*******************************************************************************
 * 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 IndexedStore {

	private static final int CurrentVersion = 1;
	private static final int MetadataID = 2;
	/*
	 * Provides the storage for the registry of stores. Key is the name the store
	 * was opened under.  The value is the store itself.  This is used to facilitate
	 * recovery in the event of a thread being killed or dying.
	 */
	private static final Map registry = Collections.synchronizedMap(new HashMap());

	private static final ObjectAddress ContextAddress10 = new ObjectAddress(1, 0);
	private static final ObjectAddress ContextAddress11 = new ObjectAddress(1, 1);

	private ObjectAddress objectDirectoryAddress; /* initialized at open */
	private Index objectDirectory; /* initialized at open */
	private IndexCursor objectDirectoryCursor; /* initialized at open */

	private ObjectAddress indexDirectoryAddress; /* initialized at open */
	private Index indexDirectory; /* initialized at open */
	private IndexCursor indexDirectoryCursor; /* initialized at open */
	private ObjectAddress contextAddress;

	private ObjectStore objectStore; /* initialized at open */
	private String name; /* initialized at open */

	/**
	 * Acquires an anchor.
	 */
	IndexAnchor acquireAnchor(ObjectAddress address) throws IndexedStoreException {
		return (IndexAnchor) acquireObject(address);
	}

	/**
	 * Acquires a context.  Returns null if the context could not be acquired.
	 */
	IndexedStoreContext acquireContext(ObjectAddress address) {
		try {
			return (IndexedStoreContext) acquireObject(address);
		} catch (IndexedStoreException e) {
			//context couldn't be acquired - return null
			return null;
		}
	}

	/**
	 * Acquire an index node.
	 */
	IndexNode acquireNode(ObjectAddress address) throws IndexedStoreException {
		return (IndexNode) acquireObject(address);
	}

	/**
	 * Acquires an object.
	 */
	private StoredObject acquireObject(ObjectAddress address) throws IndexedStoreException {
		StoredObject object;
		try {
			object = objectStore.acquireObject(address);
		} catch (ObjectStoreException e) {
			throw new IndexedStoreException(IndexedStoreException.ObjectNotAcquired, e);
		}
		return object;
	}

	/**
	 * Acquires a Binary Object.
	 */
	BinarySmallObject acquireBinarySmallObject(ObjectAddress address) throws IndexedStoreException {
		return (BinarySmallObject) acquireObject(address);
	}

	/**
	 * Checks to see if the metadata stored in the object store matches that expected by this
	 * code.  If not, a conversion is necessary.
	 */
	private void checkMetadata() throws IndexedStoreException {
		Buffer metadata = getMetadataArea(MetadataID);
		Field versionField = metadata.getField(0, 4);
		int version = versionField.getInt();
		if (version == 0) {
			// 0 indicates that the store is new
			versionField.put(CurrentVersion);
			putMetadataArea(MetadataID, metadata);
			return;
		}
		if (version == CurrentVersion)
			return;
		convert(version);
	}

	/**
	 * Closes the store.  This is required to free the underlying file.
	 */
	public synchronized void close() throws IndexedStoreException {
		if (name == null)
			return;//already closed
		try {
			commit();
			if (objectDirectoryCursor != null)
				objectDirectoryCursor.close();
			if (indexDirectoryCursor != null)
				indexDirectoryCursor.close();
		} catch (IndexedStoreException e) {
			//make sure the file gets closed no matter what
			try {
				objectStore.close();
			} catch (ObjectStoreException e2) {
				//ignore this and rethrow the underlying exception
			}
			throw e;
		}
		try {
			objectStore.close();
		} catch (ObjectStoreException e) {
			throw new IndexedStoreException(IndexedStoreException.StoreNotClosed, e);
		}
		registry.remove(name);
		name = null;
		objectDirectory = null;
		objectDirectoryAddress = null;
		objectDirectoryCursor = null;
		indexDirectory = null;
		indexDirectoryAddress = null;
		indexDirectoryCursor = null;
	}

	public synchronized void commit() throws IndexedStoreException {
		try {
			objectStore.commit();
		} catch (Exception e) {
			throw new IndexedStoreException(IndexedStoreException.StoreNotCommitted, e);
		}
	}

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

	/**
	 * Creates and initializes an IndexedStore.
	 */
	public static synchronized void create(String name) throws IndexedStoreException {
		ObjectStore store = new ObjectStore(new IndexedStoreObjectPolicy());
		try {
			ObjectStore.create(name);
			store.open(name);
			ObjectAddress contextAddress = store.insertObject(new IndexedStoreContext());
			IndexedStoreContext context = (IndexedStoreContext) store.acquireObject(contextAddress);
			IndexAnchor anchor = new IndexAnchor();
			ObjectAddress address = store.insertObject(anchor);
			context.setIndexDirectoryAddress(address);
			anchor = new IndexAnchor();
			address = store.insertObject(anchor);
			context.setObjectDirectoryAddress(address);
			context.release();
			store.commit();
			store.close();
		} catch (Exception e1) {
			try {
				store.close();
			} catch (ObjectStoreException e2) {
				//real exception thrown below
			}
			ObjectStore.delete(name);
			throw new IndexedStoreException(IndexedStoreException.StoreNotCreated, e1);
		}
	}

	/**
	 * Creates an Index with the given name.
	 */
	public synchronized Index createIndex(String indexName) throws IndexedStoreException {
		Index index = null;
		indexDirectoryCursor.find(indexName);
		if (indexDirectoryCursor.keyMatches(indexName)) {
			throw new IndexedStoreException(IndexedStoreException.IndexExists);
		}
		ObjectAddress address = insertObject(new IndexAnchor());
		indexDirectory.insert(indexName, address.toByteArray());
		index = new Index(this, address);
		return index;
	}

	/**
	 * Places a byte array into the store, return a new object identifier.
	 */
	public synchronized ObjectID createObject(byte[] b) throws IndexedStoreException {
		ObjectAddress address = insertObject(new BinarySmallObject(b));
		ObjectID id = getNextObjectID();
		objectDirectory.insert(id.toByteArray(), address.toByteArray());
		return id;
	}

	/**
	 * Places a String into the store.
	 */
	public synchronized ObjectID createObject(String s) throws IndexedStoreException {
		return createObject(Convert.toUTF8(s));
	}

	/**
	 * Tests to see if the file acting as the store exists.
	 */
	public static synchronized boolean exists(String filename) {
		return ObjectStore.exists(filename);
	}

	/**
	 * If a store disappears unexpectedly, make sure it gets closed.
	 */
	protected void finalize() {
		try {
			close();
		} catch (Exception e) {
			//unsafe to throw exceptions from a finalize
		}
	}

	/**
	 * Finds the handle of an open store for a given its name.  The store may continue with the current transaction,
	 * or may abort the current transaction.  Used to initiate recovery if the reference to the store should be
	 * lost for some reason.  Will return null if the store has not been opened.  The name of the store to be found
	 * must compare equal to the name the store was opened under.
	 */
	public synchronized static IndexedStore find(String name) {
		return (IndexedStore) registry.get(name);
	}

	/**
	 * Returns an index given its name.
	 */
	public synchronized Index getIndex(String indexName) throws IndexedStoreException {
		Index index;
		byte[] key = Convert.toUTF8(indexName);
		indexDirectoryCursor.find(key);
		if (!indexDirectoryCursor.keyMatches(key))
			throw new IndexedStoreException(IndexedStoreException.IndexNotFound);
		ObjectAddress address = indexDirectoryCursor.getValueAsObjectAddress();
		index = new Index(this, address);
		return index;
	}

	private Buffer getMetadataArea(int i) throws IndexedStoreException {
		try {
			return objectStore.getMetadataArea(i);
		} catch (ObjectStoreException e) {
			throw new IndexedStoreException(IndexedStoreException.MetadataRequestError, e);
		}
	}

	/**
	 * Returns the next ObjectID
	 */
	private ObjectID getNextObjectID() throws IndexedStoreException {
		IndexedStoreContext context = acquireContext(contextAddress);
		if (context == null)
			throw new IndexedStoreException(IndexedStoreException.ContextNotAvailable);
		long objectNumber = context.getNextObjectNumber();
		context.release();
		return new ObjectID(objectNumber);
	}

	/**
	 * Returns a byte array given its object identifier.
	 */
	public synchronized byte[] getObject(ObjectID id) throws IndexedStoreException {
		objectDirectoryCursor.find(id.toByteArray());
		ObjectAddress address = objectDirectoryCursor.getValueAsObjectAddress();
		BinarySmallObject object = acquireBinarySmallObject(address);
		byte[] b = object.getValue();
		object.release();
		return b;
	}

	/**
	 * Returns an object as a string, truncated at the first null.
	 */
	public synchronized String getObjectAsString(ObjectID id) throws IndexedStoreException {
		String s;
		s = Convert.fromUTF8(getObject(id));
		int i = s.indexOf(0);
		if (i == -1)
			return s;
		return s.substring(0, i);
	}

	/** 
	 * Inserts a new object into my store.
	 */
	ObjectAddress insertObject(StoredObject object) throws IndexedStoreException {
		try {
			ObjectAddress address = objectStore.insertObject(object);
			return address;
		} catch (ObjectStoreException e) {
			throw new IndexedStoreException(IndexedStoreException.ObjectNotStored, e);
		}
	}

	/**
	 * Opens the store.
	 */
	public synchronized void open(String name) throws IndexedStoreException {
		if (registry.get(name) != null) {
			throw new IndexedStoreException(IndexedStoreException.StoreIsOpen);
		}
		if (!exists(name))
			create(name);
		try {
			objectStore = new ObjectStore(new IndexedStoreObjectPolicy());
			objectStore.open(name);
			// setting the name signals the indexed store as open
			this.name = name;			
			checkMetadata();
			contextAddress = ContextAddress10;
			IndexedStoreContext context = acquireContext(contextAddress);
			if (context == null) {
				contextAddress = ContextAddress11;
				context = acquireContext(contextAddress);
			}
			if (context == null) {
				throw new IndexedStoreException(IndexedStoreException.StoreFormatError);
			}
			indexDirectoryAddress = context.getIndexDirectoryAddress();
			objectDirectoryAddress = context.getObjectDirectoryAddress();
			context.release();
			indexDirectory = new Index(this, indexDirectoryAddress);
			indexDirectoryCursor = indexDirectory.open();
			objectDirectory = new Index(this, objectDirectoryAddress);
			objectDirectoryCursor = objectDirectory.open();
			registry.put(name, this);
		} catch (IndexedStoreException e) {
			throw e;
		} catch (Exception e) {
			throw new IndexedStoreException(IndexedStoreException.GenericError, e);
		}
	}

	private void putMetadataArea(int i, Buffer b) throws IndexedStoreException {
		try {
			objectStore.putMetadataArea(i, b);
		} catch (ObjectStoreException e) {
			throw new IndexedStoreException(IndexedStoreException.MetadataRequestError, e);
		}
	}

	/** 
	 * Removes an object from my store.
	 */
	void removeObject(ObjectAddress address) throws IndexedStoreException {
		try {
			objectStore.removeObject(address);
		} catch (ObjectStoreException e) {
			throw new IndexedStoreException(IndexedStoreException.ObjectNotRemoved, e);
		}
	}

	/**
	 * Removes the object identified by id from the store.
	 */
	public synchronized void removeObject(ObjectID id) throws IndexedStoreException {
		byte[] key = id.toByteArray();
		objectDirectoryCursor.find(key);
		if (!objectDirectoryCursor.keyMatches(key)) {
			throw new IndexedStoreException(IndexedStoreException.ObjectNotFound);
		}
		ObjectAddress address = objectDirectoryCursor.getValueAsObjectAddress();
		objectDirectoryCursor.remove();
		removeObject(address);
	}
}
