/*******************************************************************************
 * Copyright (c) 2000, 2018 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.compare.structuremergeviewer;

import java.lang.reflect.InvocationTargetException;

import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.CompareUI;
import org.eclipse.compare.CompareViewerSwitchingPane;
import org.eclipse.compare.ICompareFilter;
import org.eclipse.compare.IContentChangeListener;
import org.eclipse.compare.IContentChangeNotifier;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.contentmergeviewer.IDocumentRange;
import org.eclipse.compare.internal.ChangeCompareFilterPropertyAction;
import org.eclipse.compare.internal.CompareMessages;
import org.eclipse.compare.internal.CompareUIPlugin;
import org.eclipse.compare.internal.Utilities;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.services.IDisposable;


/**
 * A diff tree viewer that can be configured with a <code>IStructureCreator</code>
 * to retrieve a hierarchical structure from the input object (an <code>ICompareInput</code>)
 * and perform a two-way or three-way compare on it.
 * <p>
 * This class may be instantiated; it is not intended to be subclassed outside
 * this package.
 * </p>
 *
 * @see IStructureCreator
 * @see ICompareInput
 * @noextend This class is not intended to be subclassed by clients.
 */

public class StructureDiffViewer extends DiffTreeViewer {
	private Differencer fDifferencer;
	private boolean fThreeWay= false;

	private StructureInfo fAncestorStructure = new StructureInfo();
	private StructureInfo fLeftStructure = new StructureInfo();
	private StructureInfo fRightStructure = new StructureInfo();

	private IStructureCreator fStructureCreator;
	private IDiffContainer fRoot;
	private IContentChangeListener fContentChangedListener;
	private CompareViewerSwitchingPane fParent;
	private ICompareInputChangeListener fCompareInputChangeListener;

	/*
	 * A set of background tasks for updating the structure
	 */
	private IRunnableWithProgress diffTask = monitor -> {
		monitor.beginTask(CompareMessages.StructureDiffViewer_0, 100);
		diff(SubMonitor.convert(monitor, 100));
		monitor.done();
	};

	private IRunnableWithProgress inputChangedTask = monitor -> {
		monitor.beginTask(CompareMessages.StructureDiffViewer_1, 100);
		// TODO: Should we always force
		compareInputChanged((ICompareInput) getInput(), true, SubMonitor.convert(monitor, 100));
		monitor.done();
	};

	/*
	 * A helper class for holding the input and generated structure
	 * for the ancestor, left and right inputs.
	 */
	private class StructureInfo {
		private ITypedElement fInput;
		private IStructureComparator fStructureComparator;
		private IRunnableWithProgress refreshTask = monitor -> refresh(monitor);

		public boolean setInput(ITypedElement newInput, boolean force, IProgressMonitor monitor) {
			boolean changed = false;
			if (force || newInput != fInput) {
				removeDocumentRangeUpdaters();
				if (fInput instanceof IContentChangeNotifier && fContentChangedListener != null)
					((IContentChangeNotifier)fInput).removeContentChangeListener(fContentChangedListener);
				fInput= newInput;
				if (fInput == null) {
					dispose(); // destroy fStructureComparator
					fStructureComparator= null;
				} else {
					refresh(monitor);
					changed= true;
				}
				if (fInput instanceof IContentChangeNotifier && fContentChangedListener != null)
					((IContentChangeNotifier)fInput).addContentChangeListener(fContentChangedListener);
			}
			return changed;
		}

		/**
		 * Remove any document range updaters that were registered against the document.
		 */
		private void removeDocumentRangeUpdaters() {
			if (fStructureComparator instanceof IDocumentRange) {
				IDocument doc = ((IDocumentRange) fStructureComparator).getDocument();
				try {
					doc.removePositionCategory(IDocumentRange.RANGE_CATEGORY);
				} catch (BadPositionCategoryException ex) {
					// Ignore
				}
			}
		}

		public IStructureComparator getStructureComparator() {
			return fStructureComparator;
		}

		public void refresh(IProgressMonitor monitor) {
			IStructureComparator oldComparator = fStructureComparator;
			fStructureComparator= createStructure(monitor);
			// Dispose of the old one after in case they are using a shared document
			// (i.e. disposing it after will hold on to a reference to the document
			// so it doesn't get freed and reloaded)
			if (oldComparator instanceof IDisposable) {
				IDisposable disposable = (IDisposable) oldComparator;
				disposable.dispose();
			}
		}

		public Object getInput() {
			return fInput;
		}

		private IStructureComparator createStructure(IProgressMonitor monitor) {
			// Defend against concurrent disposal
			Object input = fInput;
			if (input == null)
				return null;
			if (fStructureCreator instanceof IStructureCreator2) {
				IStructureCreator2 sc2 = (IStructureCreator2) fStructureCreator;
				try {
					return sc2.createStructure(input, monitor);
				} catch (CoreException e) {
					CompareUIPlugin.log(e);
				}
			}
			return fStructureCreator.getStructure(input);
		}

		public void dispose() {
			if (fStructureComparator != null && fStructureCreator instanceof IStructureCreator2) {
				IStructureCreator2 sc2 = (IStructureCreator2) fStructureCreator;
				sc2.destroy(fStructureComparator);
			}
		}

		public IRunnableWithProgress getRefreshTask() {
			return refreshTask;
		}
	}

	/**
	 * Creates a new viewer for the given SWT tree control with the specified configuration.
	 *
	 * @param tree the tree control
	 * @param configuration the configuration for this viewer
	 */
	public StructureDiffViewer(Tree tree, CompareConfiguration configuration) {
		super(tree, configuration);
		Composite c= tree.getParent();
		if (c instanceof CompareViewerSwitchingPane)
			fParent= (CompareViewerSwitchingPane) c;
		initialize();
	}

	/**
	 * Creates a new viewer under the given SWT parent with the specified configuration.
	 *
	 * @param parent the SWT control under which to create the viewer
	 * @param configuration the configuration for this viewer
	 */
	public StructureDiffViewer(Composite parent, CompareConfiguration configuration) {
		super(parent, configuration);
		if (parent instanceof CompareViewerSwitchingPane)
			fParent= (CompareViewerSwitchingPane) parent;
		initialize();
	}

	private void initialize() {

		setAutoExpandLevel(3);

		fContentChangedListener= changed -> StructureDiffViewer.this.contentChanged(changed);
		fCompareInputChangeListener = input -> StructureDiffViewer.this.compareInputChanged(input, true);
	}

	/**
	 * Configures the <code>StructureDiffViewer</code> with a structure creator.
	 * The structure creator is used to create a hierarchical structure
	 * for each side of the viewer's input element of type <code>ICompareInput</code>.
	 *
	 * @param structureCreator the new structure creator
	 */
	public void setStructureCreator(IStructureCreator structureCreator) {
		if (fStructureCreator != structureCreator) {
			fStructureCreator= structureCreator;
			Control tree= getControl();
			if (tree != null && !tree.isDisposed())
				tree.setData(CompareUI.COMPARE_VIEWER_TITLE, getTitle());
		}
	}

	/**
	 * Returns the structure creator or <code>null</code> if no
	 * structure creator has been set with <code>setStructureCreator</code>.
	 *
	 * @return the structure creator or <code>null</code>
	 */
	public IStructureCreator getStructureCreator() {
		return fStructureCreator;
	}

	/**
	 * Reimplemented to get the descriptive title for this viewer from the <code>IStructureCreator</code>.
	 * @return the viewer's name
	 */
	@Override
	public String getTitle() {
		if (fStructureCreator != null)
			return fStructureCreator.getName();
		return super.getTitle();
	}

	/**
	 * Overridden because the input of this viewer is not identical to the root of the tree.
	 * The tree's root is a IDiffContainer that was returned from the method <code>diff</code>.
	 *
	 * @return the root of the diff tree produced by method <code>diff</code>
	 */
	@Override
	protected Object getRoot() {
		return fRoot;
	}

    /*
     * (non-Javadoc) Method declared on StructuredViewer.
     * Overridden to create the comparable structures from the input object
	 * and to feed them through the differencing engine. Note: for this viewer
	 * the value from <code>getInput</code> is not identical to <code>getRoot</code>.
	 */
	@Override
	protected void inputChanged(Object input, Object oldInput) {
		if (oldInput instanceof ICompareInput) {
			ICompareInput old = (ICompareInput) oldInput;
			old.removeCompareInputChangeListener(fCompareInputChangeListener);
		}
		if (input instanceof ICompareInput) {
			ICompareInput ci = (ICompareInput) input;
			ci.addCompareInputChangeListener(fCompareInputChangeListener);
			compareInputChanged(ci);
			if (input != oldInput)
				initialSelection();
		}
	}

	@Override
	protected void initialSelection() {
		expandToLevel(2);
	}

	/* (non Javadoc)
	 * Overridden to unregister all listeners.
	 */
	@Override
	protected void handleDispose(DisposeEvent event) {
		Object input = getInput();
		if (input instanceof ICompareInput) {
			ICompareInput ci = (ICompareInput) input;
			ci.removeCompareInputChangeListener(fCompareInputChangeListener);
		}
		compareInputChanged(null);
		fContentChangedListener= null;
		super.handleDispose(event);
	}

	/**
	 * Recreates the comparable structures for the input sides.
	 * @param input this viewer's new input
	 */
	protected void compareInputChanged(ICompareInput input) {
		compareInputChanged(input, false);
	}

	/* package */ void compareInputChanged(final ICompareInput input, final boolean force) {
		if (input == null) {
			// When closing, we don't need a progress monitor to handle the input change
			compareInputChanged(input, force, null);
			return;
		}
		CompareConfiguration cc = getCompareConfiguration();
		// The compare configuration is nulled when the viewer is disposed
		if (cc != null) {
			BusyIndicator.showWhile(Display.getDefault(), () -> {
				try {
					inputChangedTask.run(new NullProgressMonitor());
				} catch (InvocationTargetException e1) {
					CompareUIPlugin.log(e1.getTargetException());
				} catch (InterruptedException e2) {
					// Ignore
				}
			});
		}
	}

	/* package */ void compareInputChanged(ICompareInput input, boolean force, IProgressMonitor monitor) {
		ITypedElement t= null;
		boolean changed= false;

		if (input != null)
			t= input.getAncestor();
		fThreeWay= (t != null);
		beginWork(monitor, 400);
		try {
			if (fAncestorStructure.setInput(t, force, subMonitor(monitor, 100)))
				changed = true;

			if (input != null)
				t= input.getLeft();
			if (fLeftStructure.setInput(t, force, subMonitor(monitor, 100)))
				changed = true;

			if (input != null)
				t= input.getRight();
			if (fRightStructure.setInput(t, force, subMonitor(monitor, 100)))
				changed = true;

			// The compare configuration is nulled when the viewer is disposed
			CompareConfiguration cc = getCompareConfiguration();
			if (changed && cc != null)
				cc.getContainer().runAsynchronously(diffTask);
		} finally {
			endWork(monitor);
		}
	}

	private void endWork(IProgressMonitor monitor) {
		if (monitor != null)
			monitor.done();
	}

	private IProgressMonitor subMonitor(IProgressMonitor monitor, int work) {
		if (monitor != null) {
			if (monitor.isCanceled() || getControl().isDisposed())
				throw new OperationCanceledException();
			return SubMonitor.convert(monitor, work);
		}
		return null;
	}

	private void beginWork(IProgressMonitor monitor, int totalWork) {
		if (monitor != null)
			monitor.beginTask(null, totalWork);
	}

	/**
	 * Calls <code>diff</code> whenever the byte contents changes.
	 * @param changed the object that sent out the notification
	 */
	protected void contentChanged(final IContentChangeNotifier changed) {
		if (fStructureCreator == null)
			return;

		if (changed == null) {
			getCompareConfiguration().getContainer().runAsynchronously(fAncestorStructure.getRefreshTask());
			getCompareConfiguration().getContainer().runAsynchronously(fLeftStructure.getRefreshTask());
			getCompareConfiguration().getContainer().runAsynchronously(fRightStructure.getRefreshTask());
		} else if (changed == fAncestorStructure.getInput()) {
			getCompareConfiguration().getContainer().runAsynchronously(fAncestorStructure.getRefreshTask());
		} else if (changed == fLeftStructure.getInput()) {
			getCompareConfiguration().getContainer().runAsynchronously(fLeftStructure.getRefreshTask());
		} else if (changed == fRightStructure.getInput()) {
			getCompareConfiguration().getContainer().runAsynchronously(fRightStructure.getRefreshTask());
		} else {
			return;
		}
		getCompareConfiguration().getContainer().runAsynchronously(diffTask);
	}

	/**
	 * This method is called from within <code>diff()</code> before the
	 * difference tree is being built. Clients may override this method to
	 * perform their own pre-processing. This default implementation does
	 * nothing.
	 *
	 * @param ancestor the ancestor input to the differencing operation
	 * @param left the left input to the differencing operation
	 * @param right the right input to the differencing operation
	 * @since 2.0
	 * @deprecated Clients should override
	 *             {@link #preDiffHook(IStructureComparator, IStructureComparator, IStructureComparator, IProgressMonitor)}
	 */
	@Deprecated
	protected void preDiffHook(IStructureComparator ancestor, IStructureComparator left, IStructureComparator right) {
		// we do nothing here
	}

	/**
	 * This method is called from within {@link #diff(IProgressMonitor)} before
	 * the difference tree is being built. This method may be called from a
	 * background (non-UI) thread).
	 * <p>
	 * For backwards compatibility, this default implementation calls
	 * {@link #preDiffHook(IStructureComparator, IStructureComparator, IStructureComparator)}
	 * from the UI thread. Clients should override this method even if they
	 * don't perform pre-processing to avoid the call to the UI thread.
	 *
	 * @param ancestor the ancestor input to the differencing operation
	 * @param left the left input to the differencing operation
	 * @param right the right input to the differencing operation
	 * @param monitor a progress monitor or null if progress is not required
	 * @since 3.3
	 */
	protected void preDiffHook(final IStructureComparator ancestor, final IStructureComparator left, final IStructureComparator right, IProgressMonitor monitor) {
		syncExec(() -> preDiffHook(ancestor, left, right));
	}

	/**
	 * Runs the difference engine and refreshes the tree. This method may be called
	 * from a background (non-UI) thread).
	 * @param monitor a progress monitor or <code>null</code> if progress in not required
	 */
	protected void diff(IProgressMonitor monitor) {
		try {
			beginWork(monitor, 150);

			IStructureComparator ancestorComparator = fAncestorStructure.getStructureComparator();
			IStructureComparator leftComparator = fLeftStructure.getStructureComparator();
			IStructureComparator rightComparator = fRightStructure.getStructureComparator();

			preDiffHook(ancestorComparator,
					leftComparator,
					rightComparator,
					subMonitor(monitor, 25));

			String message= null;

			if ((fThreeWay && ancestorComparator == null) || leftComparator == null || rightComparator == null) {
				// could not get structure of one (or more) of the legs
				fRoot= null;
				message= CompareMessages.StructureDiffViewer_StructureError;
			} else {	// calculate difference of the two (or three) structures
				if (fDifferencer == null)
					fDifferencer= new Differencer() {
						@Override
						protected boolean contentsEqual(Object o1,
								char contributor1, Object o2, char contributor2) {
							return StructureDiffViewer.this.contentsEqual(o1,
									contributor1, o2, contributor2);
						}
						@Override
						protected Object visit(Object data, int result, Object ancestor, Object left, Object right) {
							Object o= super.visit(data, result, ancestor, left, right);
							if (!getCompareConfiguration().isMirrored() && o instanceof DiffNode)
								((DiffNode)o).swapSides(true);
							return o;
						}
					};

				fRoot= (IDiffContainer) fDifferencer.findDifferences(fThreeWay, subMonitor(monitor, 100), null,
						ancestorComparator, leftComparator, rightComparator);

				if (fRoot == null || fRoot.getChildren().length == 0) {
					message= CompareMessages.StructureDiffViewer_NoStructuralDifferences;
				} else {
					postDiffHook(fDifferencer, fRoot, subMonitor(monitor, 25));
				}
			}

			if (Display.getCurrent() != null) {
				refreshAfterDiff(message);
			} else {
				final String theMessage = message;
				Display.getDefault().asyncExec(() -> refreshAfterDiff(theMessage));
			}
		} finally {
			endWork(monitor);
		}
	}

	private void refreshAfterDiff(String message) {
		if (getControl().isDisposed())
			return;
		if (fParent != null)
			fParent.setTitleArgument(message);

		refresh(getRoot());
		// Setting the auto-expand level doesn't do anything for refreshes
		expandToLevel(3);
	}

	/**
	 * Runs the difference engine and refreshes the tree.
	 */
	protected void diff() {
		try {
			CompareConfiguration compareConfiguration = getCompareConfiguration();
			// A null compare configuration indicates that the viewer was disposed
			if (compareConfiguration != null) {
				compareConfiguration.getContainer().run(true, true, monitor -> {
					monitor.beginTask(CompareMessages.StructureDiffViewer_2, 100);
					diffTask.run(SubMonitor.convert(monitor, 100));
					monitor.done();
				});
			}
		} catch (InvocationTargetException e) {
			// Shouldn't happen since the run doesn't throw
			CompareUIPlugin.log(e.getTargetException());
			handleFailedRefresh(e.getTargetException().getMessage());
		} catch (InterruptedException e) {
			// Canceled by user
			handleFailedRefresh(CompareMessages.StructureDiffViewer_3);
		}
	}

	private void handleFailedRefresh(final String message) {
		Runnable runnable = () -> {
			if (getControl().isDisposed())
				return;
			refreshAfterDiff(message);
		};
		if (Display.getCurrent() != null) {
			runnable.run();
		} else {
			Display.getDefault().asyncExec(runnable);
		}
	}

	/**
	 * This method is called from within <code>diff()</code> after the
	 * difference tree has been built. Clients may override this method to
	 * perform their own post-processing. This default implementation does
	 * nothing.
	 *
	 * @param differencer the differencer used to perform the differencing
	 * @param root the non-<code>null</code> root node of the difference tree
	 * @since 2.0
	 * @deprecated Subclasses should override
	 *             {@link #postDiffHook(Differencer, IDiffContainer, IProgressMonitor)}
	 *             instead
	 */
	@Deprecated
	protected void postDiffHook(Differencer differencer, IDiffContainer root) {
		// we do nothing here
	}

	/**
	 * This method is called from within {@link #diff(IProgressMonitor)} after
	 * the difference tree has been built. This method may be called from a
	 * background (non-UI) thread).
	 * <p>
	 * For backwards compatibility, this default implementation calls
	 * {@link #postDiffHook(Differencer, IDiffContainer)} from the UI thread.
	 * Clients should override this method even if they don't perform post
	 * processing to avoid the call to the UI thread.
	 *
	 * @param differencer the differencer used to perform the differencing
	 * @param root the non-<code>null</code> root node of the difference tree
	 * @param monitor a progress monitor or <code>null</code> if progress is
	 *            not required
	 * @since 3.3
	 */
	protected void postDiffHook(final Differencer differencer, final IDiffContainer root, IProgressMonitor monitor) {
		syncExec(() -> postDiffHook(differencer, root));
	}

	/*
	 * Performs a byte compare on the given objects.
	 * Called from the difference engine.
	 * Returns <code>null</code> if no structure creator has been set.
	 */
	private boolean contentsEqual(Object o1, char contributor1, Object o2,
			char contributor2) {
		if (fStructureCreator != null) {
			boolean ignoreWhiteSpace = Utilities.getBoolean(
					getCompareConfiguration(),
					CompareConfiguration.IGNORE_WHITESPACE, false);
			ICompareFilter[] compareFilters =
					Utilities.getCompareFilters(getCompareConfiguration());
			String s1, s2;
			if (compareFilters != null && compareFilters.length > 0
					&& fStructureCreator instanceof StructureCreator) {
				return ((StructureCreator) fStructureCreator).contentsEquals(
						o1, contributor1, o2, contributor2, ignoreWhiteSpace,
						compareFilters);
			}

			s1 = fStructureCreator.getContents(o1, ignoreWhiteSpace);
			s2 = fStructureCreator.getContents(o2, ignoreWhiteSpace);
			if (s1 == null || s2 == null)
				return false;
			return s1.equals(s2);
		}
		return false;
	}

	/**
	 * Tracks property changes of the configuration object.
	 * Clients may override to track their own property changes.
	 * In this case they must call the inherited method.
	 * @param event the property changed event that triggered the call to this method
	 */
	@Override
	protected void propertyChange(PropertyChangeEvent event) {
		String key= event.getProperty();
		if (key.equals(CompareConfiguration.IGNORE_WHITESPACE)) {
			diff();
		} else if (key.equals(ChangeCompareFilterPropertyAction.COMPARE_FILTERS)
				&& getCompareConfiguration().getProperty(
						ChangeCompareFilterPropertyAction.COMPARE_FILTERS_INITIALIZING) == null) {
			diff();
		} else if (key.equals("ANCESTOR_STRUCTURE_REFRESH")) { //$NON-NLS-1$
			fAncestorStructure.refresh(new NullProgressMonitor());
			diff();
		} else if (key.equals("LEFT_STRUCTURE_REFRESH")) { //$NON-NLS-1$
			fLeftStructure.refresh(new NullProgressMonitor());
			diff();
		} else if (key.equals("RIGHT_STRUCTURE_REFRESH")) { //$NON-NLS-1$
			fRightStructure.refresh(new NullProgressMonitor());
			diff();
		} else if (key.equals("ALL_STRUCTURE_REFRESH")) { //$NON-NLS-1$
			fAncestorStructure.refresh(new NullProgressMonitor());
			fLeftStructure.refresh(new NullProgressMonitor());
			fRightStructure.refresh(new NullProgressMonitor());
			diff();
		} else {
			super.propertyChange(event);
		}
	}

	/**
	 * Overridden to call the <code>save</code> method on the structure creator after
	 * nodes have been copied from one side to the other side of an input object.
	 *
	 * @param leftToRight if <code>true</code> the left side is copied to the right side.
	 * If <code>false</code> the right side is copied to the left side
	 */
	@Override
	protected void copySelected(boolean leftToRight) {
		super.copySelected(leftToRight);

		if (fStructureCreator != null) {
			fStructureCreator.save(
							leftToRight ? fRightStructure.getStructureComparator() : fLeftStructure.getStructureComparator(),
							leftToRight ? fRightStructure.getInput() : fLeftStructure.getInput());
		}
	}

	private void syncExec(final Runnable runnable) {
		if (getControl().isDisposed())
			return;
		if (Display.getCurrent() != null) {
			runnable.run();
		} else {
			getControl().getDisplay().syncExec(() -> {
				if (!getControl().isDisposed()) runnable.run();
			});
		}
	}
}

