/*******************************************************************************
 * Copyright (c) 2000, 2015 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
 *     James Blackburn (Broadcom Corp.) - ongoing development
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
 *******************************************************************************/
package org.eclipse.core.internal.resources;

import java.io.*;
import java.util.*;
import org.eclipse.core.internal.localstore.SafeChunkyInputStream;
import org.eclipse.core.internal.localstore.SafeFileInputStream;
import org.eclipse.core.internal.utils.Messages;
import org.eclipse.core.internal.utils.Policy;
import org.eclipse.core.internal.watson.IPathRequestor;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;

//
public class Synchronizer implements ISynchronizer {
	protected Workspace workspace;
	protected SyncInfoWriter writer;

	// Registry of sync partners. Set of qualified names.
	protected Set<QualifiedName> registry = new HashSet<>(5);

	public Synchronizer(Workspace workspace) {
		super();
		this.workspace = workspace;
		this.writer = new SyncInfoWriter(workspace, this);
	}

	/**
	 * @see ISynchronizer#accept(QualifiedName, IResource, IResourceVisitor, int)
	 */
	@Override
	public void accept(QualifiedName partner, IResource resource, IResourceVisitor visitor, int depth) throws CoreException {
		Assert.isLegal(partner != null);
		Assert.isLegal(resource != null);
		Assert.isLegal(visitor != null);

		// if we don't have sync info for the given identifier, then skip it
		if (getSyncInfo(partner, resource) != null) {
			// visit the resource and if the visitor says to stop the recursion then return
			if (!visitor.visit(resource))
				return;
		}

		// adjust depth if necessary
		if (depth == IResource.DEPTH_ZERO || resource.getType() == IResource.FILE)
			return;
		if (depth == IResource.DEPTH_ONE)
			depth = IResource.DEPTH_ZERO;

		// otherwise recurse over the children
		IResource[] children = ((IContainer) resource).members();
		for (IResource element : children)
			accept(partner, element, visitor, depth);
	}

	/**
	 * @see ISynchronizer#add(QualifiedName)
	 */
	@Override
	public void add(QualifiedName partner) {
		Assert.isLegal(partner != null);
		registry.add(partner);
	}

	/**
	 * @see ISynchronizer#flushSyncInfo(QualifiedName, IResource, int)
	 */
	@Override
	public void flushSyncInfo(final QualifiedName partner, final IResource root, final int depth) throws CoreException {
		Assert.isLegal(partner != null);
		Assert.isLegal(root != null);

		ICoreRunnable body = monitor -> {
			IResourceVisitor visitor = resource -> {
				//only need to flush sync info if there is sync info
				if (getSyncInfo(partner, resource) != null)
					setSyncInfo(partner, resource, null);
				return true;
			};
			root.accept(visitor, depth, true);
		};
		workspace.run(body, root, IResource.NONE, null);
	}

	/**
	 * @see ISynchronizer#getPartners()
	 */
	@Override
	public QualifiedName[] getPartners() {
		return registry.toArray(new QualifiedName[registry.size()]);
	}

	/**
	 * For use by the serialization code.
	 */
	protected Set<QualifiedName> getRegistry() {
		return registry;
	}

	/**
	 * @see ISynchronizer#getSyncInfo(QualifiedName, IResource)
	 */
	@Override
	public byte[] getSyncInfo(QualifiedName partner, IResource resource) throws CoreException {
		Assert.isLegal(partner != null);
		Assert.isLegal(resource != null);

		if (!isRegistered(partner)) {
			String message = NLS.bind(Messages.synchronizer_partnerNotRegistered, partner);
			throw new ResourceException(new ResourceStatus(IResourceStatus.PARTNER_NOT_REGISTERED, message));
		}

		// namespace check, if the resource doesn't exist then return null
		ResourceInfo info = workspace.getResourceInfo(resource.getFullPath(), true, false);
		return (info == null) ? null : info.getSyncInfo(partner, true);
	}

	protected boolean isRegistered(QualifiedName partner) {
		Assert.isLegal(partner != null);
		return registry.contains(partner);
	}

	/**
	 * @see #savePartners(DataOutputStream)
	 */
	public void readPartners(DataInputStream input) throws CoreException {
		SyncInfoReader reader = new SyncInfoReader(workspace, this);
		reader.readPartners(input);
	}

	public void restore(IResource resource, IProgressMonitor monitor) throws CoreException {
		// first restore from the last save and then apply any snapshots
		restoreFromSave(resource);
		restoreFromSnap(resource);
	}

	protected void restoreFromSave(IResource resource) throws CoreException {
		IPath sourceLocation = workspace.getMetaArea().getSyncInfoLocationFor(resource);
		IPath tempLocation = workspace.getMetaArea().getBackupLocationFor(sourceLocation);
		if (!sourceLocation.toFile().exists() && !tempLocation.toFile().exists())
			return;
		try {
			try (
				DataInputStream input = new DataInputStream(new SafeFileInputStream(sourceLocation.toOSString(), tempLocation.toOSString()));
			) {
				SyncInfoReader reader = new SyncInfoReader(workspace, this);
				reader.readSyncInfo(input);
			}
		} catch (Exception e) {
			//don't let runtime exceptions such as ArrayIndexOutOfBounds prevent startup
			String msg = NLS.bind(Messages.resources_readMeta, sourceLocation);
			throw new ResourceException(IResourceStatus.FAILED_READ_METADATA, sourceLocation, msg, e);
		}
	}

	protected void restoreFromSnap(IResource resource) {
		IPath sourceLocation = workspace.getMetaArea().getSyncInfoSnapshotLocationFor(resource);
		if (!sourceLocation.toFile().exists())
			return;
		try {
			DataInputStream input = new DataInputStream(new SafeChunkyInputStream(sourceLocation.toFile()));
			try {
				SyncInfoSnapReader reader = new SyncInfoSnapReader(workspace, this);
				while (true)
					reader.readSyncInfo(input);
			} catch (EOFException eof) {
				// ignore end of file -- proceed with what we successfully read
			} finally {
				input.close();
			}
		} catch (Exception e) {
			// only log the exception, we should not fail restoring the snapshot
			String msg = NLS.bind(Messages.resources_readMeta, sourceLocation);
			Policy.log(new ResourceStatus(IResourceStatus.FAILED_READ_METADATA, sourceLocation, msg, e));
		}
	}

	/**
	 * @see ISynchronizer#remove(QualifiedName)
	 */
	@Override
	public void remove(QualifiedName partner) {
		Assert.isLegal(partner != null);
		if (isRegistered(partner)) {
			// remove all sync info for this partner
			try {
				flushSyncInfo(partner, workspace.getRoot(), IResource.DEPTH_INFINITE);
				registry.remove(partner);
			} catch (CoreException e) {
				// XXX: flush needs to be more resilient and not throw exceptions all the time
				Policy.log(e);
			}
		}
	}

	public void savePartners(DataOutputStream output) throws IOException {
		writer.savePartners(output);
	}

	public void saveSyncInfo(ResourceInfo info, IPathRequestor requestor, DataOutputStream output, List<QualifiedName> writtenPartners) throws IOException {
		writer.saveSyncInfo(info, requestor, output, writtenPartners);
	}

	protected void setRegistry(Set<QualifiedName> registry) {
		this.registry = registry;
	}

	/**
	 * @see ISynchronizer#setSyncInfo(QualifiedName, IResource, byte[])
	 */
	@Override
	public void setSyncInfo(QualifiedName partner, IResource resource, byte[] info) throws CoreException {
		Assert.isLegal(partner != null);
		Assert.isLegal(resource != null);
		try {
			workspace.prepareOperation(resource, null);
			workspace.beginOperation(true);
			if (!isRegistered(partner)) {
				String message = NLS.bind(Messages.synchronizer_partnerNotRegistered, partner);
				throw new ResourceException(new ResourceStatus(IResourceStatus.PARTNER_NOT_REGISTERED, message));
			}
			// we do not store sync info on the workspace root
			if (resource.getType() == IResource.ROOT)
				return;
			// if the resource doesn't yet exist then create a phantom so we can set the sync info on it
			Resource target = (Resource) resource;
			ResourceInfo resourceInfo = workspace.getResourceInfo(target.getFullPath(), true, false);
			int flags = target.getFlags(resourceInfo);
			if (!target.exists(flags, false)) {
				if (info == null)
					return;
				//ensure it is possible to create this resource
				target.checkValidPath(target.getFullPath(), target.getType(), false);
				Container parent = (Container) target.getParent();
				parent.checkAccessible(parent.getFlags(parent.getResourceInfo(true, false)));
				workspace.createResource(target, true);
			}
			resourceInfo = target.getResourceInfo(true, true);
			resourceInfo.setSyncInfo(partner, info);
			resourceInfo.incrementSyncInfoGenerationCount();
			resourceInfo.set(ICoreConstants.M_SYNCINFO_SNAP_DIRTY);
			flags = target.getFlags(resourceInfo);
			if (target.isPhantom(flags) && resourceInfo.getSyncInfo(false) == null) {
				MultiStatus status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.INTERNAL_ERROR, Messages.resources_deleteProblem, null);
				((Resource) resource).deleteResource(false, status);
				if (!status.isOK())
					throw new ResourceException(status);
			}
		} finally {
			workspace.endOperation(resource, false);
		}
	}

	public void snapSyncInfo(ResourceInfo info, IPathRequestor requestor, DataOutputStream output) throws IOException {
		writer.snapSyncInfo(info, requestor, output);
	}
}
