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

import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.List;

import org.eclipse.compare.structuremergeviewer.ICompareInput;
import org.eclipse.core.resources.*;
import org.eclipse.core.resources.mapping.*;
import org.eclipse.core.runtime.*;
import org.eclipse.jface.action.*;
import org.eclipse.jface.viewers.*;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.*;
import org.eclipse.team.core.diff.IDiff;
import org.eclipse.team.core.mapping.IMergeContext;
import org.eclipse.team.core.mapping.provider.ResourceDiffTree;
import org.eclipse.team.examples.filesystem.FileSystemPlugin;
import org.eclipse.team.internal.ui.mapping.SynchronizationResourceMappingContext;
import org.eclipse.team.ui.mapping.ISynchronizationCompareAdapter;
import org.eclipse.team.ui.synchronize.ModelMergeOperation;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.Page;

/**
 * This class provides the page for the {@link NonSyncMergePart}.
 */
public class NonSyncModelMergePage extends Page {
	
	IMergeContext context;
	TreeViewer viewer;
	List mappings;
	
	/*
	 * Content provider that returns the list of conflicting mappings
	 */
	class PageContentProvider implements ITreeContentProvider {
		public Object[] getChildren(Object parentElement) {
			if (parentElement instanceof IMergeContext) {
				if (mappings == null)
					// TODO: should be using a real progress monitor
					computeMappings(new NullProgressMonitor());
				return mappings.toArray();
			}
			return new Object[0];
		}
		public Object getParent(Object element) {
			if (element instanceof ResourceMapping) {
				return context;
			}
			return null;
		}
		public boolean hasChildren(Object element) {
			if (element instanceof IMergeContext) {
				return true;
			}
			return false;
		}
		public Object[] getElements(Object inputElement) {
			return getChildren(inputElement);
		}
		public void dispose() {
			// Nothing to do
		}
		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
			// Nothing to do
		}
	}
	
	/*
	 * Label provider that provides a label and image for conflicting resource mappings
	 */
	class PageLabelProvider extends LabelProvider {
		public String getText(Object element) {
			if (element instanceof ResourceMapping) {
				ResourceMapping mapping = (ResourceMapping) element;
				ISynchronizationCompareAdapter adapter = NonSyncMergePart.getCompareAdapter(mapping);
				if (adapter != null)
					return adapter.getPathString(mapping) + "(" + mapping.getModelProvider().getDescriptor().getLabel() + ")";
			}
			if (element instanceof ICompareInput) {
				ICompareInput ci = (ICompareInput) element;
				ci.getName();
			}
			return super.getText(element);
		}
		public Image getImage(Object element) {
			if (element instanceof ICompareInput) {
				ICompareInput ci = (ICompareInput) element;
				ci.getImage();
			}
			if (element instanceof ResourceMapping) {
				ResourceMapping mapping = (ResourceMapping) element;
				ISynchronizationCompareAdapter adapter = NonSyncMergePart.getCompareAdapter(mapping);
				ICompareInput input = adapter.asCompareInput(context, mapping.getModelObject());
				if (input != null)
					return input.getImage();
			}
			return super.getImage(element);
		}
	}

	/*
	 * Sorter that sorts mappings by model and then name
	 */
	class PageSorter extends ViewerSorter {
		public int compare(Viewer viewer, Object e1, Object e2) {
			if (e1 instanceof ResourceMapping && e2 instanceof ResourceMapping) {
				ResourceMapping m1 = (ResourceMapping) e1;
				ResourceMapping m2 = (ResourceMapping) e2;
				if (m1.getModelProvider() == m2.getModelProvider()) {
					return getLabel(m1).compareTo(getLabel(m2));
				}
				return compare(m1, m2);
			}
			return super.compare(viewer, e1, e2);
		}
		private int compare(ResourceMapping m1, ResourceMapping m2) {
			ModelProvider[] sorted = ModelMergeOperation.sortByExtension(new ModelProvider[] { m1.getModelProvider(), m2.getModelProvider() });
			return sorted[0] == m1.getModelProvider() ? -1 : 1;
		}
		private String getLabel(ResourceMapping mapping) {
			ISynchronizationCompareAdapter adapter = NonSyncMergePart.getCompareAdapter(mapping);
			if (adapter != null)
				return adapter.getPathString(mapping);
			return "";
		}
	}
	
	public NonSyncModelMergePage(IMergeContext context) {
		super();
		this.context = context;
	}
	
	/**
	 * Create the list of all mappings that overlap with the out-of-sync files.
	 */
	public void computeMappings(IProgressMonitor monitor) {
	    IModelProviderDescriptor[] descriptors = ModelProvider.getModelProviderDescriptors();
	    mappings = new ArrayList();
	    for (int i = 0; i < descriptors.length; i++) {
	        IModelProviderDescriptor descriptor = descriptors[i];
	        // Get the subset of files that this model provider cares about
	        try {
				IResource[] resources = descriptor.getMatchingResources(getOutOfSyncFiles());
				if (resources.length > 0) {
				    ModelProvider provider = descriptor.getModelProvider();
				    // Get the mappings for those resources
				    ResourceMapping[] mappings = provider.getMappings(resources, new SynchronizationResourceMappingContext(context), monitor);
				    this.mappings.addAll(Arrays.asList(mappings ));
				}
			} catch (CoreException e) {
				FileSystemPlugin.log(e);
			}
	    }
	}

	private IResource[] getOutOfSyncFiles() {
		IDiff[] diffs = getContext().getDiffTree().getDiffs(ResourcesPlugin.getWorkspace().getRoot(), IResource.DEPTH_INFINITE);
		List result = new ArrayList();
		for (int i = 0; i < diffs.length; i++) {
			IDiff diff = diffs[i];
			IResource resource = ResourceDiffTree.getResourceFor(diff);
			if (resource.getType() == IResource.FILE)
				result.add(resource);
		}
		return (IResource[]) result.toArray(new IResource[result.size()]);
	}

	/**
	 * Return the merge context.
	 * @return the merge context
	 */
	public IMergeContext getContext() {
		return context;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.part.Page#createControl(org.eclipse.swt.widgets.Composite)
	 */
	public void createControl(Composite parent) {
		viewer = new TreeViewer(parent);
		viewer.setContentProvider(new PageContentProvider());
		viewer.setLabelProvider(new PageLabelProvider());
		viewer.setSorter(new PageSorter());
		hookContextMenu(viewer);
		viewer.setInput(context);
	}

	/*
	 * Hook the context menu to display the Overwrite and Mark-as-merged actions
	 */
	private void hookContextMenu(final TreeViewer viewer) {
		final MenuManager menuMgr = new MenuManager(); 
		menuMgr.setRemoveAllWhenShown(true);
		menuMgr.addMenuListener(manager -> fillContextMenu(manager));
		Menu menu = menuMgr.createContextMenu(viewer.getControl());
		viewer.getControl().setMenu(menu);
	}

	/**
	 * Fill the context menu.
	 * @param manager the context menu manager
	 */
	protected void fillContextMenu(IMenuManager manager) {
		/*
		 * Add a mark as merged action. Because we are not using the 
		 * Synchronization framework and, more specifically, the
		 * Common Navigator content provider for the model providers,
		 * we do not have access to the merge handlers of the model.
		 * Therefore, we are writing are action to detect whether there
		 * are files that overlap between the selected model elements and
		 * unselected model elements.
		 */
		Action markAsMerged = new Action("Mark as Merged") {
			public void run() {
				try {
					final IStructuredSelection selection = viewer.getStructuredSelection();
					PlatformUI.getWorkbench().getProgressService().busyCursorWhile(monitor -> {
						IDiff[] diffs = getSelectedDiffs(selection, monitor);
						if (!checkForModelOverlap(diffs, monitor)) {
							return;
						}
						try {
							context.markAsMerged(diffs, false, monitor);
						} catch (CoreException e) {
							throw new InvocationTargetException(e);
						}
					});
				} catch (InvocationTargetException e) {
					FileSystemPlugin.log(new Status(IStatus.ERROR, FileSystemPlugin.ID, 0, e.getTargetException().getMessage(), e.getTargetException()));
				} catch (InterruptedException e) {
					// Ignore
				}
			}
		};
		manager.add(markAsMerged);
	}

	protected IDiff[] getSelectedDiffs(IStructuredSelection selection, IProgressMonitor monitor) {
		Object[] elements = selection.toArray();
		return getDiffs(elements, monitor);
	}

	private IDiff[] getDiffs(Object[] elements, IProgressMonitor monitor) {
		Set result = new HashSet();
		for (int i = 0; i < elements.length; i++) {
			Object element = elements[i];
			try {
				if (element instanceof ResourceMapping) {
					ResourceMapping mapping = (ResourceMapping) element;
					ResourceTraversal[] traversals = mapping.getTraversals(new SynchronizationResourceMappingContext(context), monitor);
					result.addAll(Arrays.asList(context.getDiffTree().getDiffs(traversals)));
				}
			} catch (CoreException e) {
				FileSystemPlugin.log(e);
			}
		}
		return (IDiff[]) result.toArray(new IDiff[result.size()]);
	}

	/**
	 * Check whether any of the diffs overlap with mappings that are not selected
	 * @param diffs
	 * @return
	 */
	protected boolean checkForModelOverlap(IDiff[] diffs, IProgressMonitor monitor) {
		// TODO: This check should see if the diffs are also part of mappings 
		// that are not included in the selection. 
		return true;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.ui.part.Page#getControl()
	 */
	public Control getControl() {
		if (viewer != null)
			return viewer.getControl();
		return null;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.part.Page#setFocus()
	 */
	public void setFocus() {
		if (viewer != null)
			viewer.getControl().setFocus();
	}

	public ISelectionProvider getViewer() {
		return viewer;
	}

}
