/*******************************************************************************
 * Copyright (c) 2006, 2018 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.team.examples.model.mapping;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.mapping.ResourceMapping;
import org.eclipse.core.resources.mapping.ResourceTraversal;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.team.core.diff.FastDiffFilter;
import org.eclipse.team.core.diff.IDiff;
import org.eclipse.team.core.diff.IDiffVisitor;
import org.eclipse.team.core.diff.IThreeWayDiff;
import org.eclipse.team.core.history.IFileRevision;
import org.eclipse.team.core.mapping.IMergeContext;
import org.eclipse.team.core.mapping.IResourceDiff;
import org.eclipse.team.core.mapping.ResourceMappingMerger;
import org.eclipse.team.core.mapping.provider.MergeStatus;
import org.eclipse.team.core.mapping.provider.ResourceDiffTree;
import org.eclipse.team.core.mapping.provider.SynchronizationContext;
import org.eclipse.team.examples.filesystem.FileSystemPlugin;
import org.eclipse.team.examples.model.ModelObject;
import org.eclipse.team.examples.model.ModelObjectDefinitionFile;
import org.eclipse.team.examples.model.ModelProject;

/**
 * A resource mapping merger for our example model 
 */
public class ModelMerger extends ResourceMappingMerger {

	private final org.eclipse.team.examples.model.mapping.ExampleModelProvider provider;

	public ModelMerger(org.eclipse.team.examples.model.mapping.ExampleModelProvider provider) {
		this.provider = provider;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.team.core.mapping.ResourceMappingMerger#getModelProvider()
	 */
	protected org.eclipse.core.resources.mapping.ModelProvider getModelProvider() {
		return provider;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.team.core.mapping.ResourceMappingMerger#merge(org.eclipse.team.core.mapping.IMergeContext, org.eclipse.core.runtime.IProgressMonitor)
	 */
	public IStatus merge(IMergeContext mergeContext, IProgressMonitor monitor) throws CoreException {
		try {
			IStatus status;
			// Only override the merge for three-way synchronizations
			if (mergeContext.getType() == SynchronizationContext.THREE_WAY) {
				monitor.beginTask("Merging model elements", 100);
				status = mergeModelElements(mergeContext, SubMonitor.convert(monitor, 50));
				// Stop the merge if there was a failure
				if (!status.isOK())
					return status;
				// We need to wait for any background processing to complete for the context
				// so the diff tree will be up-to-date when we delegate the rest of the merge
				// to the superclass
				try {
					Job.getJobManager().join(mergeContext, SubMonitor.convert(monitor, 50));
				} catch (InterruptedException e) {
					// Ignore
				}
				// Delegate the rest of the merge to the superclass
				status = super.merge(mergeContext, monitor);
			} else {
				status = super.merge(mergeContext, monitor);
			}
			return status;
		} finally {
			monitor.done();
		}
	}

	/*
	 * Merge all the model element changes in the context
	 */
	private IStatus mergeModelElements(IMergeContext mergeContext, IProgressMonitor monitor) throws CoreException {
		try {
			IDiff[] modeDiffs = getModDiffs(mergeContext);
			List failures = new ArrayList();
			monitor.beginTask(null, 100 * modeDiffs.length);
			for (int i = 0; i < modeDiffs.length; i++) {
				IDiff diff = modeDiffs[i];
				if (!mergeModelElement(mergeContext, diff, SubMonitor.convert(monitor, 100))) {
					failures.add(diff);
				}
			}
			if (failures.size() > 0) {
				return new MergeStatus(FileSystemPlugin.ID, "Several objects could not be merged", getMappings(failures));
			}
			return Status.OK_STATUS;
		} finally {
			monitor.done();
		}
	}

	private ResourceMapping[] getMappings(List failures) {
		List mappings = new ArrayList();
		for (Iterator iter = failures.iterator(); iter.hasNext();) {
			IDiff diff = (IDiff) iter.next();
			IResource resource = ResourceDiffTree.getResourceFor(diff);
			ModelObjectDefinitionFile file = (ModelObjectDefinitionFile)ModelObject.create(resource);
			mappings.add(file.getAdapter(ResourceMapping.class));
		}
		return (ResourceMapping[]) mappings.toArray(new ResourceMapping[mappings.size()]);
	}

	/*
	 * Return all the diffs for MOD files.
	 */
	private IDiff[] getModDiffs(IMergeContext mergeContext) {
		final List result = new ArrayList();
		mergeContext.getDiffTree().accept(getModelProjectTraversals(mergeContext), new IDiffVisitor() {
			public boolean visit(IDiff diff) {
				IResource resource = ResourceDiffTree.getResourceFor(diff);
				if (ModelObjectDefinitionFile.isModFile(resource)) {
					result.add(diff);
				}
				return true;
			}
		
		});
		return (IDiff[]) result.toArray(new IDiff[result.size()]);
	}

	/*
	 * Return a traversal that covers all the model projects in the scope of the merge.
	 */
	private ResourceTraversal[] getModelProjectTraversals(IMergeContext mergeContext) {
		IProject[] scopeProjects = mergeContext.getScope().getProjects();
		List modelProjects = new ArrayList();
		for (int i = 0; i < scopeProjects.length; i++) {
			IProject project = scopeProjects[i];
			try {
				if (ModelProject.isModProject(project)) {
					modelProjects.add(project);
				}
			} catch (CoreException e) {
				FileSystemPlugin.log(e);
			}
		}
		if (modelProjects.isEmpty())
			return new ResourceTraversal[0];
		return new ResourceTraversal[] { 
			new ResourceTraversal((IResource[]) modelProjects.toArray(new IResource[modelProjects.size()]), 
					IResource.DEPTH_INFINITE, IResource.NONE)	
		};
	}

	/*
	 * Merge the model definition file and all the element files it contains.
	 */
	private boolean mergeModelElement(IMergeContext mergeContext, IDiff diff, IProgressMonitor monitor) throws CoreException {
		if (diff instanceof IThreeWayDiff) {
			IThreeWayDiff twd = (IThreeWayDiff) diff;
			if (twd.getDirection() == IThreeWayDiff.INCOMING
					|| twd.getDirection() == IThreeWayDiff.CONFLICTING) {
				IResource resource = ResourceDiffTree.getResourceFor(diff);
				
				// First, check if a change conflicts with a deletion
				if (twd.getDirection() == IThreeWayDiff.CONFLICTING) {
					if (!resource.exists())
						return false;
					if (((IResourceDiff)twd.getRemoteChange()).getAfterState() == null)
						return false;
				}
				
				// First determine the element files and element file changes
				IResourceDiff remoteChange = (IResourceDiff)twd.getRemoteChange();
				IResource[] localElements = getReferencedResources(resource);
				IResource[] baseElements = getReferencedResources(resource.getProject().getName(), remoteChange.getBeforeState(), monitor);
				IResource[] remoteElements = getReferencedResources(resource.getProject().getName(), remoteChange.getAfterState(), monitor);
				IResource[] addedElements = getAddedElements(baseElements, remoteElements);
				// Trick: The removed elements can be obtained by reversing the base and remote and looking for added
				IResource[] removedElements = getAddedElements(remoteElements, baseElements);
				
				// Check to see if any removed elements have changed locally
				if (hasOutgoingChanges(mergeContext, removedElements)) {
					return false;
				}
				
				// Now try to merge all the element files involved
				Set elementFiles = new HashSet();
				elementFiles.addAll(Arrays.asList(baseElements));
				elementFiles.addAll(Arrays.asList(localElements));
				elementFiles.addAll(Arrays.asList(remoteElements));
				if (!mergeElementFiles(mergeContext, (IResource[]) elementFiles.toArray(new IResource[elementFiles.size()]), monitor)) {
					return false;
				}
				
				// Finally, merge the model definition
				if (!resource.exists()) {
					// This is a new model definition so just merge it
					IStatus status = mergeContext.merge(diff, false, monitor);
					if (!status.isOK())
						return false;
				} else {
					// Update the contents of the model definition file
					ModelObjectDefinitionFile file = (ModelObjectDefinitionFile)ModelObject.create(resource);
					elementFiles = new HashSet();
					elementFiles.addAll(Arrays.asList(localElements));
					elementFiles.addAll(Arrays.asList(addedElements));
					elementFiles.removeAll(Arrays.asList(removedElements));
					file.setElements((IResource[]) elementFiles.toArray(new IResource[elementFiles.size()]));
					// Let the merge context know we handled the file
					mergeContext.markAsMerged(diff, false, monitor);
				}
			}
		}
		return true;
	}

	private boolean mergeElementFiles(IMergeContext mergeContext, IResource[] resources, IProgressMonitor monitor) throws CoreException {
		IDiff[] diffs = getDiffs(mergeContext, resources);
		IStatus status = mergeContext.merge(diffs, false, monitor);
		return status.isOK();
	}

	private IDiff[] getDiffs(IMergeContext mergeContext, IResource[] resources) {
		Set diffSet = new HashSet();
		for (int i = 0; i < resources.length; i++) {
			IResource resource = resources[i];
			IDiff[] diffs = mergeContext.getDiffTree().getDiffs(resource, IResource.DEPTH_ZERO);
			diffSet.addAll(Arrays.asList(diffs));
		}
		return (IDiff[]) diffSet.toArray(new IDiff[diffSet.size()]);
	}

	private boolean hasOutgoingChanges(IMergeContext mergeContext, IResource[] removedElements) {
		FastDiffFilter fastDiffFilter = new FastDiffFilter() {
			public boolean select(IDiff diff) {
				if (diff instanceof IThreeWayDiff) {
					IThreeWayDiff twd = (IThreeWayDiff) diff;
					return twd.getDirection() == IThreeWayDiff.OUTGOING || twd.getDirection() == IThreeWayDiff.CONFLICTING;
				}
				return false;
			}
		};
		for (int i = 0; i < removedElements.length; i++) {
			IResource resource = removedElements[i];
			if  (mergeContext.getDiffTree().hasMatchingDiffs(resource.getFullPath(), fastDiffFilter))
				return true;	
		}
		return false;
	}

	private IResource[] getAddedElements(IResource[] baseElements, IResource[] remoteElements) {
		List result = new ArrayList();
		Set base = new HashSet();
		for (int i = 0; i < baseElements.length; i++) {
			IResource resource = baseElements[i];
			base.add(resource);
		}
		for (int i = 0; i < remoteElements.length; i++) {
			IResource resource = remoteElements[i];
			if (!base.contains(resource))
				result.add(resource);
		}
		return (IResource[]) result.toArray(new IResource[result.size()]);
	}

	private IResource[] getReferencedResources(IResource resource) throws CoreException {
		if (resource instanceof IFile && resource.exists()) {
			return ModelObjectDefinitionFile.getReferencedResources(resource.getProject().getName(), (IFile) resource);
		}
		return new IResource[0];
	}
	
	private IResource[] getReferencedResources(String projectName, IFileRevision revision, IProgressMonitor monitor) throws CoreException {
		if (revision != null) {
			return ModelObjectDefinitionFile.getReferencedResources(projectName, revision.getStorage(monitor));
		} 
		return new IResource[0];
	}

}
