/*******************************************************************************
 * 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.properties;

import java.util.*;
import org.eclipse.core.internal.indexing.IndexCursor;
import org.eclipse.core.internal.indexing.ObjectID;
import org.eclipse.core.internal.resources.CompatibilityMessages;
import org.eclipse.core.internal.resources.ResourceException;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;

/**
 *
 */
public class PropertyStore {

	// The indexed store will maintain the properties
	protected IndexedStoreWrapper store = null;

	// Add directives
	public static final int CREATE = 0; // must not exist
	public static final int UPDATE = 1; // must exist
	public static final int SET_UPDATE = 2; // create if doesn't exist, update if exists
	public static final int SET_SKIP = 3; // create if doesn't exist, don't update if exists

	// Remove directives
	public static final int IGNORE_MISSING = 0;
	public static final int FAIL_MISSING = 1;

	public PropertyStore(IPath location) {
		store = new IndexedStoreWrapper(location);
	}

	protected boolean basicExists(StoreKey searchKey) throws CoreException {
		byte[] searchBytes = searchKey.toBytes();
		IndexCursor cursor = store.getCursor();
		try {
			cursor.find(searchBytes);
			boolean exists = cursor.keyEquals(searchBytes);
			cursor.close();
			return exists;
		} catch (Exception e) {
			String message = NLS.bind(CompatibilityMessages.properties_couldNotReadProp, searchKey.getQualifier(), searchKey.getLocalName());
			throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, searchKey.getResourceName().getPath(), message, e);
		}
	}

	/**
	 * The caller is responsible for ensuring that this will not produce
	 * duplicate keys in the index.
	 */
	protected void basicInsert(StoreKey key, String value) throws CoreException {
		try {
			ObjectID valueID = store.createObject(value);
			store.getIndex().insert(key.toBytes(), valueID);
		} catch (Exception e) {
			String message = NLS.bind(CompatibilityMessages.properties_couldNotWriteProp, key.getQualifier(), key.getLocalName());
			throw new ResourceException(IResourceStatus.FAILED_WRITE_LOCAL, key.getResourceName().getPath(), message, e);
		}
	}

	protected boolean basicRemove(ResourceName resourceName, QualifiedName propertyName) throws CoreException {
		StoreKey key = new StoreKey(resourceName, propertyName);
		byte[] keyBytes = key.toBytes();
		boolean wasFound = false;
		IndexCursor cursor = store.getCursor();
		try {
			cursor.find(keyBytes);
			if (cursor.keyEquals(keyBytes)) {
				wasFound = true;
				ObjectID valueID = cursor.getValueAsObjectID();
				store.removeObject(valueID);
				cursor.remove();
			}
			cursor.close();
		} catch (Exception e) {
			String message = NLS.bind(CompatibilityMessages.properties_couldNotDeleteProp, key.getQualifier(), key.getLocalName());
			throw new ResourceException(IResourceStatus.FAILED_DELETE_LOCAL, resourceName.getPath(), message, e);
		}
		return wasFound;
	}

	protected void basicUpdate(StoreKey key, String value) throws CoreException {
		byte[] keyBytes = key.toBytes();
		IndexCursor cursor = store.getCursor();
		try {
			cursor.find(keyBytes);
			if (cursor.keyEquals(keyBytes)) {
				ObjectID oldID = cursor.getValueAsObjectID();
				store.removeObject(oldID);
				ObjectID newValueId = store.createObject(value);
				cursor.updateValue(newValueId);
			}
			cursor.close();
		} catch (Exception e) {
			String message = NLS.bind(CompatibilityMessages.properties_couldNotWriteProp, key.getQualifier(), key.getLocalName());
			throw new ResourceException(IResourceStatus.FAILED_WRITE_LOCAL, key.getResourceName().getPath(), message, e);
		}
	}

	protected synchronized void commonSet(ResourceName resourceName, StoredProperty[] properties, int depth, int setMode, QueryResults failures) throws CoreException {
		if (depth == IResource.DEPTH_ZERO) {
			for (int i = 0; i < properties.length; i++) {
				StoredProperty property = properties[i];
				StoreKey key = new StoreKey(resourceName, property.getName());
				boolean exists = basicExists(key);
				if ((exists && (setMode == CREATE)) || (!exists && (setMode == UPDATE)))
					failures.add(resourceName, property);
				else if (exists && (setMode != SET_SKIP))
					basicUpdate(key, property.getStringValue());
				else
					basicInsert(key, property.getStringValue());
			}
		} else {
			Enumeration resourceNamesEnum = deepResourceNames(resourceName);
			while (resourceNamesEnum.hasMoreElements())
				commonSet((ResourceName) resourceNamesEnum.nextElement(), properties, IResource.DEPTH_ZERO, setMode, failures);
		}
	}

	/**
	 * Returns the names of all resources that are rooted at the given resource.
	 * <p>
	 * Answers an <code>Enumeration</code> of <code>IResourceName</code>.
	 * The enumerator will include (at least) <code>resourceName</code> if it
	 * exists.  If <code>resourceName</code> does not exist returns an empty
	 * enumerator.
	 *
	 * @see ResourceName
	 * @param resourceName the name of the top most resource to match.
	 * @return an enumeration of matching resource names.
	 */
	public Enumeration deepResourceNames(ResourceName resourceName) throws CoreException {
		final Set resultHolder = new HashSet(10);
		IVisitor visitor = new IVisitor() {
			public void visit(ResourceName resourceName, StoredProperty property, IndexCursor cursor) {
				resultHolder.add(resourceName);
			}

			public boolean requiresValue(ResourceName resourceName, QualifiedName propertyName) {
				return false;
			}
		};
		recordsDeepMatching(resourceName, visitor);
		return Collections.enumeration(resultHolder);
	}

	/**
	 * Returns the named property for the given resource.
	 * <p>
	 * The retieval is performed to depth zero.  Returns <code>null</code>
	 * if there is no such property defined on the resource.
	 *
	 * @param resourceName the resource name to match.
	 * @param propertyName the property name to match.
	 * @return the matching property, or <code>null</code> if no such property.
	 */
	public StoredProperty get(ResourceName resourceName, final QualifiedName propertyName) throws CoreException {
		final Object[] resultHolder = new Object[1];
		IVisitor simpleVisitor = new IVisitor() {
			public void visit(ResourceName resourceName, StoredProperty property, IndexCursor cursor) {
				resultHolder[0] = property;
			}

			public boolean requiresValue(ResourceName resourceName, QualifiedName propertyName) {
				return true;
			}
		};
		recordsMatching(resourceName, propertyName, simpleVisitor);
		return (StoredProperty) resultHolder[0];
	}

	/**
	 * Returns all the properties for a given resource.
	 * <p>
	 * Answer a <code>QueryResults</code> containing <code>StoredProperty</code>.
	 * If there are no matches returns an empty <code>QueryResults</code></p>
	 * <p>
	 * The depth parameter allows searching based on resource name path prefix.</p>
	 *
	 * @see QueryResults
	 * @param resourceName the resource name to match.
	 * @param depth the scope of the query
	 * @return a <code>QueryResults</code> with the matching properties.
	 */
	public QueryResults getAll(ResourceName resourceName, int depth) throws CoreException {
		final QueryResults result = new QueryResults();
		IVisitor visitor = new IVisitor() {
			public void visit(ResourceName resourceName, StoredProperty property, IndexCursor cursor) {
				result.add(resourceName, property);
			}

			public boolean requiresValue(ResourceName resourceName, QualifiedName propertyName) {
				return true;
			}
		};
		if (depth == IResource.DEPTH_ZERO)
			recordsMatching(resourceName, visitor);
		else
			recordsDeepMatching(resourceName, visitor);
		return result;
	}

	/**
	 * Returns all the property names for a given resource.
	 * <p>
	 * The result is a <code>QueryResults</code> containing <code>QualifiedName</code>.  
	 * If the resource has no defined properties, the method returns
	 * an empty <code>QueryResults</code>.</p>
	 * <p>
	 * The depth parameter allows searching based on resource name path prefix.</p>
	 *
	 * @param resourceName the resource name to match.
	 * @param depth the depth to which the query runs.
	 * @return a <code>QueryResults</code> containing the property names.
	 */
	public QueryResults getNames(ResourceName resourceName, int depth) throws CoreException {
		QueryResults results = new QueryResults();
		if (depth == IResource.DEPTH_ZERO)
			recordsMatching(resourceName, propertyNameVisitor(results));
		else
			recordsDeepMatching(resourceName, propertyNameVisitor(results));
		return results;
	}

	/**
	 * Returns true if the property store is up and running.  Returns false if
	 * the store has been shutdown.
	 */
	public boolean isRunning() {
		return store != null;
	}

	protected IVisitor propertyNameVisitor(final QueryResults results) {
		return new IVisitor() {
			public void visit(ResourceName resourceName, StoredProperty property, IndexCursor cursor) {
				results.add(resourceName, property.getName());
			}

			public boolean requiresValue(ResourceName resourceName, QualifiedName propertyName) {
				return false;
			}
		};
	}

	/**
	 * Matches all properties for a given resource.
	 */
	protected void recordsDeepMatching(ResourceName resourceName, IVisitor visitor) throws CoreException {

		// Build the partial 'search' key
		StoreKey searchKey = new StoreKey(resourceName, true);
		byte[] searchBytes = searchKey.toBytes();
		int probe = searchBytes.length;
		// Position a cursor over the first matching key
		IndexCursor cursor = store.getCursor();
		try {
			cursor.find(searchBytes);

			// While we have a prefix match
			while (cursor.keyMatches(searchBytes)) {
				// Must check that the prefix is up to a valid path segment
				// note that the matching bytes length is > search key length since
				//      properties MUST have a local name.
				byte[] matchingBytes = cursor.getKey();
				if (probe == 1 || //empty path is a valid prefix for all paths
						(matchingBytes[probe] == 0) || // a full path match
						(matchingBytes[probe] == 47 /*IPath.SEPARATOR*/)) {
					// a segment boundary match
					visitPropertyAt(cursor, visitor);
				}
				// else the match is intra-segment and therefore invalid
				cursor.next();
			}
			cursor.close();
		} catch (Exception e) {
			throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, resourceName.getPath(), CompatibilityMessages.properties_storeProblem, e);
		}
	}

	/**
	 * Matches all properties for a given resource.
	 */
	protected void recordsMatching(ResourceName resourceName, IVisitor visitor) throws CoreException {

		// Build the partial 'search' key
		StoreKey searchKey = new StoreKey(resourceName, false);
		byte[] searchBytes = searchKey.toBytes();
		// Position a cursor over the first matching key
		IndexCursor cursor = store.getCursor();
		try {
			cursor.find(searchBytes);

			// While we have a prefix match, evaluate the visitor
			while (cursor.keyMatches(searchBytes)) {
				visitPropertyAt(cursor, visitor);
				cursor.next();
			}
			cursor.close();
		} catch (Exception e) {
			store.reset();
			throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, resourceName.getPath(), CompatibilityMessages.properties_storeProblem, e);
		}
	}

	/**
	 * Matches the given property for a given resource.
	 * Note that there should be only one.
	 */
	protected void recordsMatching(ResourceName resourceName, QualifiedName propertyName, IVisitor visitor) throws CoreException {

		// Build the full 'search' key
		StoreKey searchKey = new StoreKey(resourceName, propertyName);
		byte[] searchBytes = searchKey.toBytes();
		// Position a cursor over the first matching key
		IndexCursor cursor = store.getCursor();
		try {
			cursor.find(searchBytes);

			// If we have an exact match, evaluate the visitor
			if (cursor.keyEquals(searchBytes))
				visitPropertyAt(cursor, visitor);
			cursor.close();
		} catch (Exception e) {
			store.reset();
			throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, resourceName.getPath(), CompatibilityMessages.properties_storeProblem, e);
		}
	}

	/**
	 * Remove the given collection of named properties from the given resource.
	 * <p>
	 * All of the properties being removed must exist already on the
	 * resource based on the removeRule parameter.  If the rule is
	 * MISSING_IGNORE then attempts to remove properties that do not exist
	 * are ignored, if the rule is MISSING_FAIL the method will throw
	 * a <code>PropertyNotFoundException</code> if the property does not exist.
	 * <p>
	 * If an exception is thrown, all properties that did previously
	 * exist will have been removed from the resource.  To determine which
	 * properties caused the exception see the offenders result in the exception.</p>
	 * <p>
	 * The depth parameter allows matching based on resource name path prefix.</p>
	 *
	 * @param resourceName the resource containing the properties.
	 * @param propertyNames the property names to remove.
	 * @param depth the scope for matching the resource name.
	 * @param removeRule the behavior when removing non-existant properties.
	 * @exception CoreException
	 */
	public QueryResults remove(ResourceName resourceName, QualifiedName[] propertyNames, int depth, int removeRule) throws CoreException {
		QueryResults failures = new QueryResults();
		if (depth == IResource.DEPTH_ZERO) {
			for (int i = 0; i < propertyNames.length; i++) {
				boolean found = basicRemove(resourceName, propertyNames[i]);
				if (!found && (removeRule == FAIL_MISSING))
					failures.add(resourceName, propertyNames[i]);
			}
		} else {
			Enumeration resourceNamesEnum = deepResourceNames(resourceName);
			while (resourceNamesEnum.hasMoreElements()) {
				ResourceName resName = (ResourceName) resourceNamesEnum.nextElement();
				for (int i = 0; i < propertyNames.length; i++) {
					boolean found = basicRemove(resName, propertyNames[i]);
					if (!found && (removeRule == FAIL_MISSING))
						failures.add(resName, propertyNames[i]);
				}
			}
		}
		return failures;
	}

	/**
	 * Remove the named property from the given resource.
	 * <p>
	 * If a matching property does not exist on this resource
	 * the method has no affect on the store.  Removal is performed
	 * to depth zero.</p>
	 * <p>
	 * @param resourceName the resource containing the property.
	 * @param propertyName the property to remove.
	 */
	public void remove(ResourceName resourceName, QualifiedName propertyName) throws CoreException {
		remove(resourceName, new QualifiedName[] {propertyName}, IResource.DEPTH_ZERO, IGNORE_MISSING);
	}

	/**
	 * Remove all the properties from a given resource.
	 * <p>
	 * The depth parameter allows matching based on resource name path prefix.</p>
	 *
	 * @param resourceName the resource containing the properties.
	 * @param depth the scope for matching the resource name.
	 */
	public void removeAll(ResourceName resourceName, int depth) throws CoreException {
		QueryResults namesSearch = getNames(resourceName, depth);
		Enumeration resourceNamesEnum = namesSearch.getResourceNames();
		while (resourceNamesEnum.hasMoreElements()) {
			ResourceName resName = (ResourceName) resourceNamesEnum.nextElement();
			Enumeration propertyNamesEnum = Collections.enumeration(namesSearch.getResults(resName));
			while (propertyNamesEnum.hasMoreElements()) {
				QualifiedName propertyName = (QualifiedName) propertyNamesEnum.nextElement();
				basicRemove(resName, propertyName);
			}
		}
	}

	/**
	 * Sets the given collection of properties on the given resource.
	 * <p>
	 * The addRule determines whether the properties must already exist
	 * or not, and if they do whether they are updated by subsequent addition.  
	 * Valid addRule values are defined in <code>IPropertyCollectionConstants</code>.
	 * <p>
	 * The depth parameter allows matching based on resource name path prefix.</p>
	 * <p>
	 * The <code>PropertyExistsException</code> is thrown if the matching resource
	 * already has a property of the same name, and the rule requires that
	 * it must not.  If the exception is thrown, all successfull properties
	 * will have been set, and the failures are listed in the exception.</p>
	 *
	 * @param resourceName the resource to receive the properties.
	 * @param properties the properties to add.
	 * @param depth the depth at which to apply the add opertion.
	 * @param mode the behavior of the add operation.
	 * @exception CoreException
	 */
	public QueryResults set(ResourceName resourceName, StoredProperty[] properties, int depth, int mode) throws CoreException {
		QueryResults failures = new QueryResults();
		commonSet(resourceName, properties, depth, mode, failures);
		return failures;
	}

	/**
	 * Sets the given property to the given resource.
	 * <p>
	 * The property is added to depth zero.  If the resource already has
	 * a property with the same name, its value is updated to the given
	 * value (i.e. SET_UPDATE add rule equivalent.)</p>
	 *
	 * @param resourceName the resource to receive the property.
	 * @param property the property to add.
	 */
	public void set(ResourceName resourceName, StoredProperty property) throws CoreException {
		commonSet(resourceName, new StoredProperty[] {property}, IResource.DEPTH_ZERO, SET_UPDATE, null);
	}

	public void shutdown(IProgressMonitor monitor) {
		if (store == null)
			return;
		try {
			store.close();
		} finally {
			//null the store so other threads with a handle on it cannot use it
			store = null;
		}
	}

	public void startup(IProgressMonitor monitor) {
		//do nothing
	}

	protected void visitPropertyAt(IndexCursor cursor, IVisitor visitor) throws CoreException {
		try {
			StoreKey key = new StoreKey(cursor.getKey());
			ResourceName resourceName = key.getResourceName();
			QualifiedName propertyName = key.getPropertyName();
			String propertyValue = null;
			if (visitor.requiresValue(resourceName, propertyName))
				propertyValue = store.getObjectAsString(cursor.getValueAsObjectID());
			visitor.visit(resourceName, new StoredProperty(propertyName, propertyValue), cursor);
		} catch (Exception e) {
			throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, null, CompatibilityMessages.properties_storeProblem, e);
		}
	}

	public void commit() throws CoreException {
		store.commit();
	}
}
