/*******************************************************************************
 * Copyright (c) 2004, 2016 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
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
 *     Mickael Istria (Red Hat Inc.) - Bug 488937
 *******************************************************************************/
package org.eclipse.core.internal.properties;

import java.io.File;
import java.util.*;
import org.eclipse.core.internal.localstore.Bucket;
import org.eclipse.core.internal.localstore.Bucket.Entry;
import org.eclipse.core.internal.localstore.BucketTree;
import org.eclipse.core.internal.properties.PropertyBucket.PropertyEntry;
import org.eclipse.core.internal.resources.*;
import org.eclipse.core.internal.utils.Messages;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;

/**
 * @see org.eclipse.core.internal.properties.IPropertyManager
 */
public class PropertyManager2 implements IPropertyManager {
	private static final int MAX_VALUE_SIZE = 2 * 1024;

	class PropertyCopyVisitor extends Bucket.Visitor {
		private List<PropertyEntry> changes = new ArrayList<>();
		private IPath destination;
		private IPath source;

		public PropertyCopyVisitor(IPath source, IPath destination) {
			this.source = source;
			this.destination = destination;
		}

		@Override
		public void afterSaving(Bucket bucket) throws CoreException {
			saveChanges((PropertyBucket) bucket);
			changes.clear();
		}

		private void saveChanges(PropertyBucket bucket) throws CoreException {
			if (changes.isEmpty())
				return;
			// make effective all changes collected
			Iterator<PropertyEntry> i = changes.iterator();
			PropertyEntry entry = i.next();
			tree.loadBucketFor(entry.getPath());
			bucket.setProperties(entry);
			while (i.hasNext())
				bucket.setProperties(i.next());
			bucket.save();
		}

		@Override
		public int visit(Entry entry) {
			PropertyEntry sourceEntry = (PropertyEntry) entry;
			IPath destinationPath = destination.append(sourceEntry.getPath().removeFirstSegments(source.segmentCount()));
			PropertyEntry destinationEntry = new PropertyEntry(destinationPath, sourceEntry);
			changes.add(destinationEntry);
			return CONTINUE;
		}
	}

	BucketTree tree;

	public PropertyManager2(Workspace workspace) {
		this.tree = new BucketTree(workspace, new PropertyBucket());
	}

	@Override
	public void closePropertyStore(IResource target) throws CoreException {
		// ensure any uncommitted are written to disk
		tree.getCurrent().save();
		// flush in-memory state to avoid confusion if another project is later
		// created with the same name
		tree.getCurrent().flush();
	}

	@Override
	public synchronized void copy(IResource source, IResource destination, int depth) throws CoreException {
		copyProperties(source.getFullPath(), destination.getFullPath(), depth);
	}

	/**
	 * Copies all properties from the source path to the target path, to the given depth.
	 */
	private void copyProperties(final IPath source, final IPath destination, int depth) throws CoreException {
		Assert.isLegal(source.segmentCount() > 0);
		Assert.isLegal(destination.segmentCount() > 0);
		Assert.isLegal(source.segmentCount() > 1 || destination.segmentCount() == 1);

		// copy history by visiting the source tree
		PropertyCopyVisitor copyVisitor = new PropertyCopyVisitor(source, destination);
		tree.accept(copyVisitor, source, BucketTree.DEPTH_INFINITE);
	}

	@Override
	public synchronized void deleteProperties(IResource target, int depth) throws CoreException {
		tree.accept(new PropertyBucket.Visitor() {
			@Override
			public int visit(Entry entry) {
				entry.delete();
				return CONTINUE;
			}
		}, target.getFullPath(), depth == IResource.DEPTH_INFINITE ? BucketTree.DEPTH_INFINITE : depth);
	}

	@Override
	public void deleteResource(IResource target) throws CoreException {
		deleteProperties(target, IResource.DEPTH_INFINITE);
	}

	@Override
	public synchronized Map<QualifiedName, String> getProperties(IResource target) throws CoreException {
		final Map<QualifiedName, String> result = new HashMap<>();
		tree.accept(new PropertyBucket.Visitor() {
			@Override
			public int visit(Entry entry) {
				PropertyEntry propertyEntry = (PropertyEntry) entry;
				int propertyCount = propertyEntry.getOccurrences();
				for (int i = 0; i < propertyCount; i++)
					result.put(propertyEntry.getPropertyName(i), propertyEntry.getPropertyValue(i));
				return CONTINUE;
			}
		}, target.getFullPath(), BucketTree.DEPTH_ZERO);
		return result;
	}

	@Override
	public synchronized String getProperty(IResource target, QualifiedName name) throws CoreException {
		if (name.getQualifier() == null) {
			String message = Messages.properties_qualifierIsNull;
			throw new ResourceException(IResourceStatus.FAILED_READ_METADATA, target.getFullPath(), message, null);
		}
		IPath resourcePath = target.getFullPath();
		PropertyBucket current = (PropertyBucket) tree.getCurrent();
		tree.loadBucketFor(resourcePath);
		return current.getProperty(resourcePath, name);
	}

	public BucketTree getTree() {
		return tree;
	}

	public File getVersionFile() {
		return tree.getVersionFile();
	}

	@Override
	public synchronized void setProperty(IResource target, QualifiedName name, String value) throws CoreException {
		//resource may have been deleted concurrently
		//must check for existence within synchronized method
		Resource resource = (Resource) target;
		ResourceInfo info = resource.getResourceInfo(false, false);
		int flags = resource.getFlags(info);
		resource.checkAccessible(flags);
		// enforce the limit stated by the spec
		if (value != null && value.length() > MAX_VALUE_SIZE) {
			String message = NLS.bind(Messages.properties_valueTooLong, new Object[] {name.getQualifier(), name.getLocalName(), Integer.toString(MAX_VALUE_SIZE)});
			throw new ResourceException(IResourceStatus.FAILED_WRITE_METADATA, target.getFullPath(), message, null);
		}
		if (name.getQualifier() == null) {
			String message = Messages.properties_qualifierIsNull;
			throw new ResourceException(IResourceStatus.FAILED_WRITE_METADATA, target.getFullPath(), message, null);
		}

		IPath resourcePath = target.getFullPath();
		tree.loadBucketFor(resourcePath);
		PropertyBucket current = (PropertyBucket) tree.getCurrent();
		current.setProperty(resourcePath, name, value);
		current.save();
	}

	@Override
	public void shutdown(IProgressMonitor monitor) throws CoreException {
		tree.close();
	}

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