/*******************************************************************************
 * Copyright (c) 2015 Google, Inc 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:
 * 	   Sergey Prigogin (Google) - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.core.dom.parser.cpp.semantics;

import static org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.SemanticUtil.CVTYPE;
import static org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.SemanticUtil.TDEF;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.ast.ASTVisitor;
import org.eclipse.cdt.core.dom.ast.DOMException;
import org.eclipse.cdt.core.dom.ast.EScopeKind;
import org.eclipse.cdt.core.dom.ast.IASTBinaryExpression;
import org.eclipse.cdt.core.dom.ast.IASTDeclarator;
import org.eclipse.cdt.core.dom.ast.IASTEqualsInitializer;
import org.eclipse.cdt.core.dom.ast.IASTExpression;
import org.eclipse.cdt.core.dom.ast.IASTIdExpression;
import org.eclipse.cdt.core.dom.ast.IASTImplicitDestructorName;
import org.eclipse.cdt.core.dom.ast.IASTImplicitDestructorNameOwner;
import org.eclipse.cdt.core.dom.ast.IASTImplicitName;
import org.eclipse.cdt.core.dom.ast.IASTImplicitNameOwner;
import org.eclipse.cdt.core.dom.ast.IASTInitializer;
import org.eclipse.cdt.core.dom.ast.IASTInitializerClause;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTStatement;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.IScope;
import org.eclipse.cdt.core.dom.ast.IType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTExpression;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTNewExpression;
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.core.dom.ast.cpp.ICPPReferenceType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPVariable;
import org.eclipse.cdt.core.parser.util.ArrayUtil;
import org.eclipse.cdt.internal.core.dom.parser.ASTNode;
import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPASTImplicitDestructorName;
import org.eclipse.cdt.internal.core.dom.parser.cpp.ClassTypeHelper;
import org.eclipse.cdt.internal.core.dom.parser.cpp.ClassTypeHelper.MethodKind;
import org.eclipse.cdt.internal.core.dom.parser.cpp.ICPPASTInternalScope;

/**
 * Finds destructor calls for temporaries and local variables.
 */
public class DestructorCallCollector {
	/**
	 * Returns the implicit names corresponding to the destructor calls for temporaries destroyed at the end
	 * of the expression. Only expression that is not a subexpression of another expression may contain
	 * implicit destructor calls.
	 *
	 * @param expression the expression to get the destructor calls for
	 * @return an array of destructor names
	 */
	public static IASTImplicitDestructorName[] getTemporariesDestructorCalls(ICPPASTExpression expression) {
		if (expression.getParent() instanceof ICPPASTExpression)
			return IASTImplicitDestructorName.EMPTY_NAME_ARRAY; // Not a full-expression
		TemporariesDestructorCollector collector = new TemporariesDestructorCollector(expression);
		expression.accept(collector);
		return collector.getDestructorCalls();
	}

	/**
	 * Returns the implicit names corresponding to the destructor calls for the local variables destroyed at
	 * the end of the statement.
	 *
	 * @param statement the expression to get the destructor calls for
	 * @return an array of destructor names
	 */
	public static IASTImplicitDestructorName[] getLocalVariablesDestructorCalls(IASTStatement statement) {
		if (!(statement instanceof IASTImplicitDestructorNameOwner))
			return IASTImplicitDestructorName.EMPTY_NAME_ARRAY;
		LocalVariablesDestructorCollector collector =
				new LocalVariablesDestructorCollector((IASTImplicitDestructorNameOwner) statement);
		statement.accept(collector);
		return collector.getDestructorCalls();
	}

	// Not instantiatable. All methods are static.
	private DestructorCallCollector() {
	}

	private abstract static class DestructorCollector extends ASTVisitor {
		protected final IASTImplicitDestructorNameOwner owner;
		private IASTImplicitDestructorName[] destructorNames = IASTImplicitDestructorName.EMPTY_NAME_ARRAY;

		DestructorCollector(IASTImplicitDestructorNameOwner owner) {
			this.owner = owner;
		}

		IASTImplicitDestructorName[] getDestructorCalls() {
			destructorNames = ArrayUtil.trim(destructorNames);
			return destructorNames;
		}

		static ICPPMethod findDestructor(ICPPClassType classType, IASTNode point) {
			return ClassTypeHelper.getMethodInClass(classType, MethodKind.DTOR, point);
		}

		protected void addDestructorCall(IASTName name, ICPPMethod destructor) {
			CPPASTImplicitDestructorName destructorName =
					new CPPASTImplicitDestructorName(destructor.getNameCharArray(), owner, name);
			destructorName.setBinding(destructor);
			ASTNode parentNode = (ASTNode) owner;
			int offset = parentNode.getOffset() + parentNode.getLength();
			if (!(owner instanceof ICPPASTExpression))
				offset--;  // Before the closing brace.
			destructorName.setOffsetAndLength(offset, 0);
			destructorNames = ArrayUtil.prepend(destructorNames, destructorName);
		}
	}

	private static class TemporariesDestructorCollector extends DestructorCollector {
		private TemporariesDestructorCollector(ICPPASTExpression owner) {
			super(owner);
			shouldVisitImplicitNames = true;
		}

		@Override
		public int visit(IASTName name) {
			if (name instanceof IASTImplicitName && !(name.getParent() instanceof ICPPASTNewExpression)) {
				IBinding binding = name.resolveBinding();
				if (binding instanceof ICPPConstructor) {
					ICPPClassType classType = ((ICPPConstructor) binding).getClassOwner();
					ICPPMethod destructor = findDestructor(classType, name);
					if (destructor != null && !isBoundToVariable(name)) {
						addDestructorCall(name, destructor);
					}
				}
			}
			return PROCESS_CONTINUE;
		}

		private boolean isBoundToVariable(IASTName name) {
			IASTNode parent = name.getParent();
			if (!(parent instanceof IASTExpression))
				return false;

			parent = parent.getParent();
			if (!(parent instanceof IASTBinaryExpression))
				return false;

			IASTBinaryExpression binExpr = (IASTBinaryExpression) parent;
			if (binExpr.getOperator() != IASTBinaryExpression.op_assign)
				return false;

			IASTExpression left = binExpr.getOperand1();
			if (left instanceof IASTIdExpression) {
				IASTName name2 = ((IASTIdExpression) left).getName();
				IBinding binding = name2.resolveBinding();
				if (binding instanceof ICPPVariable) {
					return true;
				}
			}
			return false;
		}
	}

	private static class LocalVariablesDestructorCollector extends DestructorCollector {
		private LocalVariablesDestructorCollector(IASTImplicitDestructorNameOwner owner) {
			super(owner);
			shouldVisitNames = true;
			shouldVisitImplicitNames = true;
		}

		@Override
		public int visit(IASTName name) {
			if (name.getPropertyInParent() == IASTDeclarator.DECLARATOR_NAME) {
				IBinding binding = name.resolveBinding();
				if (binding instanceof ICPPVariable) {
					ICPPVariable var = (ICPPVariable) binding;
					try {
						IScope scope = var.getScope();
						if (scope.getKind() == EScopeKind.eLocal && scope instanceof ICPPASTInternalScope) {
							IASTNode scopeNode = ((ICPPASTInternalScope) scope).getPhysicalNode();
							if (scopeNode.equals(owner)) {
								IType type = SemanticUtil.getNestedType(var.getType(), TDEF | CVTYPE);
								if (type instanceof ICPPClassType) {
									ICPPMethod destructor = findDestructor((ICPPClassType) type, name);
									addDestructorCall(name, destructor);
								} else if (type instanceof ICPPReferenceType) {
									IASTDeclarator decl = (IASTDeclarator) name.getParent();
									addDestructorCallForTemporaryBoundToReference(decl);
								}
							}
						}
					} catch (DOMException e) {
						CCorePlugin.log(e);
					}
				}
			}
			return PROCESS_CONTINUE;
		}

		private void addDestructorCallForTemporaryBoundToReference(IASTDeclarator decl) {
			IASTInitializer initializer = decl.getInitializer();
			if (initializer instanceof IASTEqualsInitializer) {
				IASTInitializerClause clause = ((IASTEqualsInitializer) initializer).getInitializerClause();
				if (clause instanceof IASTImplicitNameOwner) {
					IASTImplicitName[] implicitNames = ((IASTImplicitNameOwner) clause).getImplicitNames();
					if (implicitNames.length != 0) {
						IASTImplicitName name = implicitNames[0];
						IBinding binding = name.resolveBinding();
						if (binding instanceof ICPPConstructor) {
							ICPPClassType classType = ((ICPPConstructor) binding).getClassOwner();
							ICPPMethod destructor = findDestructor(classType, name);
							if (destructor != null) {
								addDestructorCall(name, destructor);
							}
						}
					}
				}
			}
		}
	}
}
