/*******************************************************************************
 * Copyright (c) 2000, 2016 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.jdt.internal.ui.compare;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;

import org.eclipse.core.resources.IResource;

import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;

import org.eclipse.ui.services.IDisposable;

import org.eclipse.compare.CompareUI;
import org.eclipse.compare.IEditableContent;
import org.eclipse.compare.IEditableContentExtension;
import org.eclipse.compare.IResourceProvider;
import org.eclipse.compare.ISharedDocumentAdapter;
import org.eclipse.compare.IStreamContentAccessor;
import org.eclipse.compare.structuremergeviewer.DiffNode;
import org.eclipse.compare.structuremergeviewer.Differencer;
import org.eclipse.compare.structuremergeviewer.DocumentRangeNode;
import org.eclipse.compare.structuremergeviewer.ICompareInput;
import org.eclipse.compare.structuremergeviewer.IDiffContainer;
import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.compare.structuremergeviewer.IStructureComparator;
import org.eclipse.compare.structuremergeviewer.StructureCreator;
import org.eclipse.compare.structuremergeviewer.StructureRootNode;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.compiler.IScanner;
import org.eclipse.jdt.core.compiler.ITerminalSymbols;
import org.eclipse.jdt.core.compiler.InvalidInputException;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;


import org.eclipse.jdt.ui.text.IJavaPartitions;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;


public class JavaStructureCreator extends StructureCreator {

	private Map<String, String> fDefaultCompilerOptions;

	/**
	 * A root node for the structure. It is similar to {@link StructureRootNode} but needed
	 * to be a subclass of {@link JavaNode} because of the code used to build the structure.
	 */
	private final class RootJavaNode extends JavaNode implements IDisposable {

		private final Object fInput;
		private final boolean fEditable;
		private final ISharedDocumentAdapter fAdapter;

		private RootJavaNode(IDocument document, boolean editable, Object input, ISharedDocumentAdapter adapter) {
			super(document);
			this.fEditable = editable;
			fInput= input;
			fAdapter= adapter;
		}

		@Override
		public boolean isEditable() {
			return fEditable;
		}

		@Override
		protected void nodeChanged(DocumentRangeNode node) {
			save(this, fInput);
		}

		@Override
		public void dispose() {
			if (fAdapter != null) {
				fAdapter.disconnect(fInput);
			}
		}

		@SuppressWarnings("unchecked")
		@Override
		public <T> T getAdapter(Class<T> adapter) {
			if (adapter == ISharedDocumentAdapter.class) {
				return (T) fAdapter;
			}
			return super.getAdapter(adapter);
		}


		@Override
		public boolean isReadOnly() {
			if (fInput instanceof IEditableContentExtension) {
				IEditableContentExtension ext = (IEditableContentExtension) fInput;
				return ext.isReadOnly();
			}
			return super.isReadOnly();
		}

		@Override
		public IStatus validateEdit(Shell shell) {
			if (fInput instanceof IEditableContentExtension) {
				IEditableContentExtension ext = (IEditableContentExtension) fInput;
				return ext.validateEdit(shell);
			}
			return super.validateEdit(shell);
		}
	}

	/**
	 * RewriteInfos are used temporarily when rewriting the diff tree
	 * in order to combine similar diff nodes ("smart folding").
	 */
	static class RewriteInfo {

		boolean fIsOut= false;

		JavaNode fAncestor= null;
		JavaNode fLeft= null;
		JavaNode fRight= null;

		ArrayList<IDiffElement> fChildren= new ArrayList<>();

		void add(IDiffElement diff) {
			fChildren.add(diff);
		}

		void setDiff(ICompareInput diff) {
			if (fIsOut)
				return;

			fIsOut= true;

			JavaNode a= (JavaNode) diff.getAncestor();
			JavaNode y= (JavaNode) diff.getLeft();
			JavaNode m= (JavaNode) diff.getRight();

			if (a != null) {
				if (fAncestor != null)
					return;
				fAncestor= a;
			}
			if (y != null) {
				if (fLeft != null)
					return;
				fLeft= y;
			}
			if (m != null) {
				if (fRight != null)
					return;
				fRight= m;
			}

			fIsOut= false;
		}

		/**
		 * @return true if some nodes could be successfully combined into one
		 */
		boolean matches() {
			return !fIsOut && fAncestor != null && fLeft != null && fRight != null;
		}
	}

	public JavaStructureCreator() {
	}

	void setDefaultCompilerOptions(Map<String, String> compilerSettings) {
		fDefaultCompilerOptions= compilerSettings;
	}

	/**
	 * @return the name that appears in the enclosing pane title bar
	 */
	@Override
	public String getName() {
		return CompareMessages.JavaStructureViewer_title;
	}

	/**
	 * @param input implement the IStreamContentAccessor interface
	 * @return a tree of JavaNodes for the given input.
	 * In case of error null is returned.
	 */
	@Override
	public IStructureComparator getStructure(final Object input) {
		String contents= null;
		char[] buffer= null;
		IDocument doc= CompareUI.getDocument(input);
		if (doc == null) {
			if (input instanceof IStreamContentAccessor) {
				IStreamContentAccessor sca= (IStreamContentAccessor) input;
				try {
					contents= JavaCompareUtilities.readString(sca);
				} catch (CoreException ex) {
					// return null indicates the error.
					return null;
				}
			}

			if (contents != null) {
				int n= contents.length();
				buffer= new char[n];
				contents.getChars(0, n, buffer, 0);

				doc= new Document(contents);
				setupDocument(doc);
			}
		}

		return createStructureComparator(input, buffer, doc, null, null);
	}

	@Override
	protected IStructureComparator createStructureComparator(Object element,
			IDocument document, ISharedDocumentAdapter sharedDocumentAdapter,
			IProgressMonitor monitor) throws CoreException {
		return createStructureComparator(element, null, document, sharedDocumentAdapter, monitor);
	}

	private IStructureComparator createStructureComparator(final Object input, char[] buffer, IDocument doc, ISharedDocumentAdapter adapter, IProgressMonitor monitor) {
		String contents;
		Map<String, String> compilerOptions= null;

		if (input instanceof IResourceProvider) {
			IResource resource= ((IResourceProvider) input).getResource();
			if (resource != null) {
				IJavaElement element= JavaCore.create(resource);
				if (element != null) {
					IJavaProject javaProject= element.getJavaProject();
					if (javaProject != null)
						compilerOptions= javaProject.getOptions(true);
				}
			}
		}
		if (compilerOptions == null)
			compilerOptions= fDefaultCompilerOptions;

		if (doc != null) {
			boolean isEditable= false;
			if (input instanceof IEditableContent)
				isEditable= ((IEditableContent) input).isEditable();

			// we hook into the root node to intercept all node changes
			JavaNode root= new RootJavaNode(doc, isEditable, input, adapter);

			if (buffer == null) {
				contents= doc.get();
				int n= contents.length();
				buffer= new char[n];
				contents.getChars(0, n, buffer, 0);
			}

			ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);
			if (compilerOptions != null)
				parser.setCompilerOptions(compilerOptions);
			parser.setSource(buffer);
			parser.setFocalPosition(0);
			CompilationUnit cu= (CompilationUnit) parser.createAST(monitor);
			cu.accept(new JavaParseTreeBuilder(root, buffer, true));

			return root;
		}
		return null;
	}

	/**
	 * Returns the contents of the given node as a string.
	 * This string is used to test the content of a Java element
	 * for equality. Is is never shown in the UI, so any string representing
	 * the content will do.
	 * @param node must implement the IStreamContentAccessor interface
	 * @param ignoreWhiteSpace if <code>true</code> all Java white space (including comments) is removed from the contents.
	 * @return contents for equality test
	 */
	@Override
	public String getContents(Object node, boolean ignoreWhiteSpace) {

		if (! (node instanceof IStreamContentAccessor))
			return null;


		IStreamContentAccessor sca= (IStreamContentAccessor) node;
		String content= null;
		try {
			content= JavaCompareUtilities.readString(sca);
		} catch (CoreException ex) {
			JavaPlugin.log(ex);
			return null;
		}


		if (!ignoreWhiteSpace)
			return content;

		// replace whitespace by a single blank
		StringBuilder buf= new StringBuilder();
		char[] b= content.toCharArray();

		// to avoid the trouble when dealing with Unicode
		// we use the Java scanner to extract non-whitespace and comment tokens
		IScanner scanner= ToolFactory.createScanner(true, false, false, false); // however we request Whitespace and Comments
		scanner.setSource(b);
		try {
			int token;
			while ((token= scanner.getNextToken()) != ITerminalSymbols.TokenNameEOF) {
				char[] currentTokenSource= scanner.getCurrentTokenSource();
				switch (token) {
					case ITerminalSymbols.TokenNameCOMMENT_LINE:
					case ITerminalSymbols.TokenNameCOMMENT_BLOCK:
					case ITerminalSymbols.TokenNameCOMMENT_JAVADOC:
						StringBuilder tokenStr= new StringBuilder(currentTokenSource.length);
						tokenStr.append(currentTokenSource);
						StringTokenizer tokenizer= new StringTokenizer(tokenStr.toString());
						while (tokenizer.hasMoreTokens()) {
							buf.append(tokenizer.nextToken());
							buf.append(' ');
						}
						break;
					default:
						buf.append(currentTokenSource);
						buf.append(' ');
						break;
				}
			}
			content= buf.toString(); // success!
		} catch (InvalidInputException ex) {
			// NeedWork
		}
		return content;
	}

	/**
	 * @return true since this IStructureCreator can rewrite the diff tree
	 * in order to fold certain combinations of additions and deletions.
	 */
	public boolean canRewriteTree() {
		return true;
	}

	/**
	 * Tries to detect certain combinations of additions and deletions as renames or signature
	 * changes and folders them into a single node.
	 *
	 * @param differencer the differencer
	 * @param root the diff container root
	 */
	public void rewriteTree(Differencer differencer, IDiffContainer root) {

		HashMap<String, RewriteInfo> map= new HashMap<>(10);

		for (IDiffElement child : root.getChildren()) {
			DiffNode diff= (DiffNode) child;
			JavaNode jn= (JavaNode) diff.getId();
			if (jn == null)
				continue;
			int type= jn.getTypeCode();
			// we can only combine methods or constructors
			if (type == JavaNode.METHOD || type == JavaNode.CONSTRUCTOR) {

				// find or create a RewriteInfo for all methods with the same name
				String name= jn.extractMethodName();
				RewriteInfo nameInfo= map.get(name);
				if (nameInfo == null) {
					nameInfo= new RewriteInfo();
					map.put(name, nameInfo);
				}
				nameInfo.add(diff);

				// find or create a RewriteInfo for all methods with the same
				// (non-empty) argument list
				String argList= jn.extractArgumentList();
				RewriteInfo argInfo= null;
				if (argList != null && !"()".equals(argList)) { //$NON-NLS-1$
					argInfo= map.get(argList);
					if (argInfo == null) {
						argInfo= new RewriteInfo();
						map.put(argList, argInfo);
					}
					argInfo.add(diff);
				}

				switch (diff.getKind() & Differencer.CHANGE_TYPE_MASK) {
				case Differencer.ADDITION:
				case Differencer.DELETION:
					// we only consider addition and deletions
					// since a rename or argument list change looks
					// like a pair of addition and deletions
					if (type != JavaNode.CONSTRUCTOR)
						nameInfo.setDiff(diff);

					if (argInfo != null)
						argInfo.setDiff(diff);
					break;
				default:
					break;
				}
			}
			// recurse
			rewriteTree(differencer, diff);
		}

		// now we have to rebuild the diff tree according to the combined
		// changes
		Iterator<String> it= map.keySet().iterator();
		while (it.hasNext()) {
			String name= it.next();
			RewriteInfo i= map.get(name);
			if (i.matches()) { // we found a RewriteInfo that could be successfully combined

				// we have to find the differences of the newly combined node
				// (because in the first pass we only got a deletion and an addition)
				DiffNode d= (DiffNode) differencer.findDifferences(true, null, root, i.fAncestor, i.fLeft, i.fRight);
				if (d != null) {// there better should be a difference
					d.setDontExpand(true);
					Iterator<IDiffElement> it2= i.fChildren.iterator();
					while (it2.hasNext()) {
						IDiffElement rd= it2.next();
						root.removeToRoot(rd);
						d.add(rd);
					}
				}
			}
		}
	}

	/**
	 * The JavaHistoryAction uses this function to determine whether
	 * a selected Java element can be replaced by some piece of
	 * code from the local history.
	 * @param je Java element
	 * @return true if the given IJavaElement maps to a JavaNode
	 */
	static boolean hasEdition(IJavaElement je) {
		return JavaElementHistoryPageSource.hasEdition(je);
	}

	@Override
	protected IDocumentPartitioner getDocumentPartitioner() {
		return JavaCompareUtilities.createJavaPartitioner();
	}

	@Override
	protected String getDocumentPartitioning() {
		return IJavaPartitions.JAVA_PARTITIONING;
	}

	@Override
	protected String[] getPath(Object element, Object input) {
		if (element instanceof IJavaElement) {
			IJavaElement je = (IJavaElement) element;
			// build a path starting at the given Java element and walk
			// up the parent chain until we reach a IWorkingCopy or ICompilationUnit
			List<String> args= new ArrayList<>();
			while (je != null) {
				// each path component has a name that uses the same
				// conventions as a JavaNode name
				String name= JavaCompareUtilities.getJavaElementID(je);
				if (name == null)
					return null;
				args.add(name);
				if (je instanceof ICompilationUnit)
					break;
				je= je.getParent();
			}

			// revert the path
			int n= args.size();
			String[] path= new String[n];
			for (int i= 0; i < n; i++)
				path[i]= args.get(n-1-i);

			return path;
		}
		return null;
	}
}
