/*******************************************************************************
 * Copyright (c) 2007, 2014 Google, Inc 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:
 * 	   Sergey Prigogin (Google) - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.search;

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

import org.eclipse.cdt.core.dom.ast.ASTVisitor;
import org.eclipse.cdt.core.dom.ast.DOMException;
import org.eclipse.cdt.core.dom.ast.IASTComment;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorElseStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorEndifStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIfStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIfdefStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIfndefStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorMacroDefinition;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorUndefStatement;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.IMacroBinding;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTTemplateId;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPConstructor;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPMethod;
import org.eclipse.cdt.internal.core.dom.parser.ASTTranslationUnit;
import org.eclipse.cdt.internal.core.dom.parser.cpp.ClassTypeHelper;
import org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.CPPSemantics;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;

/**
 * Finds locations of linked names. Used by Rename in File and Rename in Workspace.
 */
public class LinkedNamesFinder {
	private static final IRegion[] EMPTY_LOCATIONS_ARRAY = new IRegion[0];

	private LinkedNamesFinder() {
		super();
	}

	public static IRegion[] findByName(IASTTranslationUnit root, IASTName name) {
		IBinding target = name.resolveBinding();
		if (target == null) {
			return EMPTY_LOCATIONS_ARRAY;
		}
		try {
			CPPSemantics.pushLookupPoint(name);
			BindingFinder bindingFinder = new BindingFinder(root, name);
			bindingFinder.find(target);
			return bindingFinder.getLocations();
		} finally {
			CPPSemantics.popLookupPoint();
		}
	}

	private static class BindingFinder {
		private final IASTTranslationUnit root;
		private final List<IRegion> locations;

		public BindingFinder(IASTTranslationUnit root, IASTNode point) {
			this.root = root;
			locations = new ArrayList<>();
		}

		public void find(IBinding target) {
			if (target instanceof IMacroBinding) {
				findMacro((IMacroBinding) target);
				return;
			}

			if (target instanceof ICPPConstructor
					|| target instanceof ICPPMethod && ((ICPPMethod) target).isDestructor()) {
				target = ((ICPPMethod) target).getClassOwner();
			}

			findBinding(target);
			if (target instanceof ICPPClassType) {
				ICPPConstructor[] constructors = ((ICPPClassType) target).getConstructors();
				for (ICPPConstructor ctor : constructors) {
					if (!ctor.isImplicit()) {
						findBinding(ctor);
					}
				}
				ICPPMethod[] methods = ((ICPPClassType) target).getDeclaredMethods();
				for (ICPPMethod method : methods) {
					if (method.isDestructor()) {
						findBinding(method);
					}
				}
			} else if (target instanceof ICPPMethod) {
				ICPPMethod method = (ICPPMethod) target;
				for (ICPPMethod m : ClassTypeHelper.findOverridden(method)) {
					findBinding(m);
				}
				try {
					for (ICPPMethod m : findOverridersInAST(method)) {
						findBinding(m);
					}
				} catch (DOMException e) {
					// Ignore.
				}
			}
		}

		private ICPPMethod[] findOverridersInAST(ICPPMethod method) throws DOMException {
			if (!ClassTypeHelper.isVirtual(method))
				return ICPPMethod.EMPTY_CPPMETHOD_ARRAY;

			final ICPPClassType ownerClass = method.getClassOwner();
			if (ownerClass == null)
				return ICPPMethod.EMPTY_CPPMETHOD_ARRAY;

			SubclassFinder subclassFinder = new SubclassFinder(ownerClass);
			root.accept(subclassFinder);
			return ClassTypeHelper.findOverriders(subclassFinder.getSubclasses(), method);
		}

		public IRegion[] getLocations() {
			if (locations.isEmpty()) {
				return EMPTY_LOCATIONS_ARRAY;
			}
			return locations.toArray(new IRegion[locations.size()]);
		}

		private void findBinding(IBinding target) {
			IASTName[] names = root.getDeclarationsInAST(target);
			for (IASTName candidate : names) {
				if (candidate.isPartOfTranslationUnitFile()) {
					addLocation(candidate);
				}
			}
			names = root.getReferences(target);
			for (IASTName candidate : names) {
				if (candidate.isPartOfTranslationUnitFile()) {
					addLocation(candidate);
				}
			}
		}

		private void addLocation(IASTName name) {
			IBinding binding = name.resolveBinding();
			if (binding != null) {
				if (name instanceof ICPPASTTemplateId) {
					name = ((ICPPASTTemplateId) name).getTemplateName();
				}
				IASTFileLocation fileLocation = name.getImageLocation();
				if (fileLocation == null || !root.getFilePath().equals(fileLocation.getFileName())) {
					fileLocation = name.getFileLocation();
				}
				if (fileLocation != null) {
					int offset = fileLocation.getNodeOffset();
					int length = fileLocation.getNodeLength();
					if (binding instanceof ICPPMethod && ((ICPPMethod) binding).isDestructor()) {
						// Skip tilde.
						offset++;
						length--;
					}
					if (offset >= 0 && length > 0) {
						locations.add(new Region(offset, length));
					}
				}
			}
		}

		/**
		 * Adds all occurrences of a macro name to the list of locations. Macro occurrences
		 * may belong to multiple macro bindings with the same name. Macro names are also
		 * looked for in the comments of #else and #endif statements.
		 * Comments of #else and #endif statements related to #ifdef or #ifndef are searched
		 * for the macro name referenced by the #if[n]def.
		 * @param target a binding representing a macro.
		 */
		private void findMacro(IMacroBinding target) {
			findBinding(target);
			char[] nameChars = target.getNameCharArray();
			List<IASTName> ifdefNameStack = new ArrayList<>();
			IASTPreprocessorStatement[] statements = root.getAllPreprocessorStatements();
			for (IASTPreprocessorStatement statement : statements) {
				if (!statement.isPartOfTranslationUnitFile()) {
					continue;
				}
				IASTName macroName = null;
				boolean ifStatement = false;
				if (statement instanceof IASTPreprocessorIfdefStatement) {
					macroName = ((IASTPreprocessorIfdefStatement) statement).getMacroReference();
					ifStatement = true;
				} else if (statement instanceof IASTPreprocessorIfndefStatement) {
					macroName = ((IASTPreprocessorIfndefStatement) statement).getMacroReference();
					ifStatement = true;
				} else if (statement instanceof IASTPreprocessorMacroDefinition) {
					macroName = ((IASTPreprocessorMacroDefinition) statement).getName();
				} else if (statement instanceof IASTPreprocessorUndefStatement) {
					macroName = ((IASTPreprocessorUndefStatement) statement).getMacroName();
				} else if (statement instanceof IASTPreprocessorIfStatement) {
					ifStatement = true;
				} else if (statement instanceof IASTPreprocessorEndifStatement) {
					if (!ifdefNameStack.isEmpty())
						if (ifdefNameStack.remove(ifdefNameStack.size() - 1) != null) {
							findInStatementComment(nameChars, statement);
						}
				} else if (statement instanceof IASTPreprocessorElseStatement) {
					if (!ifdefNameStack.isEmpty())
						if (ifdefNameStack.get(ifdefNameStack.size() - 1) != null) {
							findInStatementComment(nameChars, statement);
						}
				}
				if (macroName != null) {
					if (Arrays.equals(nameChars, macroName.getSimpleID())) {
						IBinding binding = macroName.resolveBinding();
						if (!target.equals(binding)) {
							findBinding(binding);
						}
					} else {
						macroName = null;
					}
				}
				if (ifStatement) {
					ifdefNameStack.add(macroName);
				}
			}
		}

		/**
		 * Finds locations of a given name in the comment of a preprocessor statement.
		 */
		private void findInStatementComment(char[] nameChars, IASTPreprocessorStatement statement) {
			IASTFileLocation location = statement.getFileLocation();
			IASTComment comment = findComment(location.getNodeOffset() + location.getNodeLength());
			if (comment != null
					&& comment.getFileLocation().getStartingLineNumber() == location.getStartingLineNumber()) {
				findInComment(nameChars, comment);
			}
		}

		/**
		 * Returns the first comment after the given offset.
		 * @param startOffset a file offset.
		 * @return a comment or <code>null</code>, if there are no comments after the offset.
		 */
		private IASTComment findComment(int startOffset) {
			IASTComment[] comments = ((ASTTranslationUnit) root).getComments();
			int low = 0;
			int high = comments.length;
			while (low < high) {
				int mid = (low + high) >>> 1;
				int offset = comments[mid].getFileLocation().getNodeOffset();
				if (offset < startOffset) {
					low = mid + 1;
				} else {
					high = mid;
					if (offset == startOffset) {
						break;
					}
				}
			}
			return high < comments.length ? comments[high] : null;
		}

		/**
		 * Adds all occurrences of a name in a comment to the list of locations.
		 */
		private void findInComment(char[] name, IASTComment comment) {
			char[] text = comment.getComment();
			int j = 0;
			// First two characters are either /* or //
			for (int i = 2; i <= text.length - name.length + j; i++) {
				char c = text[i];
				if (!Character.isJavaIdentifierPart(c)) {
					j = 0;
				} else if (j >= 0 && j < name.length && name[j] == c) {
					j++;
					if (j == name.length && (i + 1 == text.length || !Character.isJavaIdentifierPart(text[i + 1]))) {
						int offset = comment.getFileLocation().getNodeOffset() + i + 1 - name.length;
						locations.add(new Region(offset, name.length));
						j = 0;
					}
				} else {
					j = -1;
				}
			}
		}
	}

	/**
	 * Finds subclasses of the given class referenced by the AST.
	 */
	static class SubclassFinder extends ASTVisitor {
		{
			shouldVisitNames = true;
		}

		private final ICPPClassType baseClass;
		private Set<ICPPClassType> subclasses = new HashSet<>();
		private Set<IBinding> seenClasses = new HashSet<>();

		SubclassFinder(ICPPClassType baseClass) {
			this.baseClass = baseClass;
		}

		@Override
		public int visit(IASTName name) {
			IBinding binding = name.resolveBinding();
			if (binding instanceof ICPPClassType) {
				if (seenClasses.add(binding)) {
					try {
						CPPSemantics.pushLookupPoint(name);
						ICPPClassType candidate = (ICPPClassType) binding;
						if (ClassTypeHelper.isSubclass(candidate, baseClass)) {
							subclasses.add(candidate);
						}
					} finally {
						CPPSemantics.popLookupPoint();
					}
				}
			}
			return PROCESS_CONTINUE;
		}

		public ICPPClassType[] getSubclasses() {
			return subclasses.toArray(new ICPPClassType[subclasses.size()]);
		}
	}
}
